# HG changeset patch # User Eric Blake # Date 1253324346 21600 # Node ID 2bd4d1ff29e9dccd728bb6f0456f873d25451efc # Parent 1b85d0321310263c0f812b885e20691f42e5f295 openat: fix unlinkat bugs on Solaris 9 unlinkat(fd,"file/",0) mistakenly succeeded. * lib/unlinkat.c (unlinkat): New file. * modules/openat (Depends-on): Add unlink. (Files): Distribute it. * m4/openat.m4 (gl_FUNC_OPENAT): Mark unlinkat for replacement if trailing slash behavior is broken. * m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness. * modules/unistd (Makefile.am): Substitute it. * lib/unistd.in.h (unlinkat): Declare replacement. * doc/posix-functions/unlinkat.texi (unlinkat): Document this. Signed-off-by: Eric Blake diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,16 @@ 2009-09-19 Eric Blake + openat: fix unlinkat bugs on Solaris 9 + * lib/unlinkat.c (unlinkat): New file. + * modules/openat (Depends-on): Add unlink. + (Files): Distribute it. + * m4/openat.m4 (gl_FUNC_OPENAT): Mark unlinkat for replacement if + trailing slash behavior is broken. + * m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness. + * modules/unistd (Makefile.am): Substitute it. + * lib/unistd.in.h (unlinkat): Declare replacement. + * doc/posix-functions/unlinkat.texi (unlinkat): Document this. + openat: fix fstatat bugs on Solaris 9 * lib/fstatat.c (rpl_fstatat): Copy recent fixes from lstat and stat. diff --git a/doc/posix-functions/unlinkat.texi b/doc/posix-functions/unlinkat.texi --- a/doc/posix-functions/unlinkat.texi +++ b/doc/posix-functions/unlinkat.texi @@ -13,8 +13,35 @@ glibc 2.3.6, MacOS X 10.3, FreeBSD 6.0, NetBSD 3.0, OpenBSD 3.8, AIX 5.1, HP-UX 11, IRIX 6.5, OSF/1 5.1, Cygwin 1.5.x, mingw, Interix 3.5, BeOS. But the replacement function is not safe to be used in libraries and is not multithread-safe. +@item +Some systems mistakenly succeed on @code{unlinkat(fd,"file/",flag)}: +Solaris 9. @end itemize Portability problems not fixed by Gnulib: @itemize +@item +When @code{unlinkat(fd,name,AT_REMOVEDIR)} fails because the specified +directory is not empty, the @code{errno} value is system dependent. +@item +POSIX requires that @code{unlinkdir(fd,"link-to-empty/",AT_REMOVEDIR)} +remove @file{empty} and leave @file{link-to-empty} as a dangling +symlink. This is counter-intuitive, so some systems fail with +@code{ENOTDIR} instead: +glibc +@item +Some systems allow a superuser to unlink directories, even though this +can cause file system corruption. The error given if a process is not +permitted to unlink directories varies across implementations; it is +not always the POSIX value of @code{EPERM}. Meanwhile, if a process +has the ability to unlink directories, POSIX requires that +@code{unlinkat(fd,"symlink-to-dir/",0)} remove @file{dir} and leave +@file{symlink-to-dir} dangling; this behavior is counter-intuitive. +The gnulib module unlinkdir can help determine whether code must be +cautious of unlinking directories. +@item +Removing an open file is non-portable: On Unix this allows the programs that +have the file already open to continue working with it; the file's storage +is only freed when the no process has the file open any more. On Windows, +the attempt to remove an open file fails. @end itemize diff --git a/lib/unistd.in.h b/lib/unistd.in.h --- a/lib/unistd.in.h +++ b/lib/unistd.in.h @@ -180,7 +180,11 @@ #if @GNULIB_UNLINKAT@ -# if !@HAVE_UNLINKAT@ +# if @REPLACE_UNLINKAT@ +# undef unlinkat +# define unlinkat rpl_unlinkat +# endif +# if !@HAVE_UNLINKAT@ || @REPLACE_UNLINKAT@ extern int unlinkat (int fd, char const *file, int flag); # endif #elif defined GNULIB_POSIXCHECK diff --git a/lib/unlinkat.c b/lib/unlinkat.c new file mode 100644 --- /dev/null +++ b/lib/unlinkat.c @@ -0,0 +1,77 @@ +/* Work around unlinkat bugs on Solaris 9. + + Copyright (C) 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 + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Eric Blake. */ + +#include + +#include + +#include +#include +#include +#include + +#include "openat.h" + +#undef unlinkat + +/* unlinkat without AT_REMOVEDIR does not honor trailing / on Solaris + 9. Solve it in a similar manner to unlink. */ + +int +rpl_unlinkat (int fd, char const *name, int flag) +{ + size_t len; + int result = 0; + /* rmdir behavior has no problems with trailing slash. */ + if (flag & AT_REMOVEDIR) + return unlinkat (fd, name, flag); + + len = strlen (name); + if (len && ISSLASH (name[len - 1])) + { + /* See the lengthy comment in unlink.c why we disobey the POSIX + rule of letting unlink("link-to-dir/") attempt to unlink a + directory. */ + struct stat st; + result = lstatat (fd, name, &st); + if (result == 0) + { + /* Trailing NUL will overwrite the trailing slash. */ + char *short_name = malloc (len); + if (!short_name) + { + errno = EPERM; + return -1; + } + memcpy (short_name, name, len); + while (len && ISSLASH (short_name[len - 1])) + short_name[--len] = '\0'; + if (len && (lstatat (fd, short_name, &st) || S_ISLNK (st.st_mode))) + { + free (short_name); + errno = EPERM; + return -1; + } + free (short_name); + } + } + if (!result) + result = unlinkat (fd, name, flag); + return result; +} diff --git a/m4/openat.m4 b/m4/openat.m4 --- a/m4/openat.m4 +++ b/m4/openat.m4 @@ -1,4 +1,4 @@ -# serial 21 +# serial 22 # See if we need to use our replacement for Solaris' openat et al functions. dnl Copyright (C) 2004-2009 Free Software Foundation, Inc. @@ -30,8 +30,12 @@ case $ac_cv_func_openat+$ac_cv_func_lstat_dereferences_slashed_symlink in yes+yes) ;; yes+*) + # Solaris 9 has *at functions, but uniformly mishandles trailing + # slash in all of them. AC_LIBOBJ([fstatat]) REPLACE_FSTATAT=1 + AC_LIBOBJ([unlinkat]) + REPLACE_UNLINKAT=1 ;; *) HAVE_OPENAT=0 diff --git a/m4/unistd_h.m4 b/m4/unistd_h.m4 --- a/m4/unistd_h.m4 +++ b/m4/unistd_h.m4 @@ -1,4 +1,4 @@ -# unistd_h.m4 serial 28 +# unistd_h.m4 serial 29 dnl Copyright (C) 2006-2009 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -101,6 +101,7 @@ REPLACE_LSEEK=0; AC_SUBST([REPLACE_LSEEK]) REPLACE_RMDIR=0; AC_SUBST([REPLACE_RMDIR]) REPLACE_UNLINK=0; AC_SUBST([REPLACE_UNLINK]) + REPLACE_UNLINKAT=0; AC_SUBST([REPLACE_UNLINKAT]) REPLACE_WRITE=0; AC_SUBST([REPLACE_WRITE]) UNISTD_H_HAVE_WINSOCK2_H=0; AC_SUBST([UNISTD_H_HAVE_WINSOCK2_H]) UNISTD_H_HAVE_WINSOCK2_H_AND_USE_SOCKETS=0; diff --git a/modules/openat b/modules/openat --- a/modules/openat +++ b/modules/openat @@ -11,6 +11,7 @@ lib/openat.h lib/openat-priv.h lib/openat-proc.c +lib/unlinkat.c m4/openat.m4 m4/mode_t.m4 @@ -33,6 +34,7 @@ stdbool sys_stat unistd +unlink configure.ac: gl_FUNC_OPENAT diff --git a/modules/unistd b/modules/unistd --- a/modules/unistd +++ b/modules/unistd @@ -93,6 +93,7 @@ -e 's|@''REPLACE_LSEEK''@|$(REPLACE_LSEEK)|g' \ -e 's|@''REPLACE_RMDIR''@|$(REPLACE_RMDIR)|g' \ -e 's|@''REPLACE_UNLINK''@|$(REPLACE_UNLINK)|g' \ + -e 's|@''REPLACE_UNLINKAT''@|$(REPLACE_UNLINKAT)|g' \ -e 's|@''REPLACE_WRITE''@|$(REPLACE_WRITE)|g' \ -e 's|@''UNISTD_H_HAVE_WINSOCK2_H''@|$(UNISTD_H_HAVE_WINSOCK2_H)|g' \ -e 's|@''UNISTD_H_HAVE_WINSOCK2_H_AND_USE_SOCKETS''@|$(UNISTD_H_HAVE_WINSOCK2_H_AND_USE_SOCKETS)|g' \