# HG changeset patch # User Eric Blake # Date 1257459180 25200 # Node ID b156d2d1b827dbd84c413298045c81b283869a15 # Parent 86929e33bdadaf6b4ee91e03df6823464ce753c9 freopen-safer: new module * modules/freopen-safer: New module. * m4/stdio-safer.m4 (gl_FREOPEN_SAFER): New macro. * lib/freopen-safer.c (freopen_safer): New file. * lib/stdio-safer.h (freopen_safer): New declaration. * lib/stdio--.h (freopen): New override. * MODULES.html.sh (File stream based Input/Output): Mention it. * doc/posix-functions/freopen.texi (freopen): Mention pitfalls and freopen-safer module. * doc/posix-functions/stderr.texi (stderr): Likewise. * doc/posix-functions/stdin.texi (stdin): Likewise. * doc/posix-functions/stdout.texi (stdout): Likewise. * modules/freopen-safer-tests: New test. * tests/test-reopen-safer.c: New file. Signed-off-by: Eric Blake diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2009-11-05 Eric Blake + + freopen-safer: new module + * modules/freopen-safer: New module. + * m4/stdio-safer.m4 (gl_FREOPEN_SAFER): New macro. + * lib/freopen-safer.c (freopen_safer): New file. + * lib/stdio-safer.h (freopen_safer): New declaration. + * lib/stdio--.h (freopen): New override. + * MODULES.html.sh (File stream based Input/Output): Mention it. + * doc/posix-functions/freopen.texi (freopen): Mention pitfalls and + freopen-safer module. + * doc/posix-functions/stderr.texi (stderr): Likewise. + * doc/posix-functions/stdin.texi (stdin): Likewise. + * doc/posix-functions/stdout.texi (stdout): Likewise. + * modules/freopen-safer-tests: New test. + * tests/test-reopen-safer.c: New file. + 2009-11-05 Jim Meyering maint.mk: Prohibit inclusion of "close-stream.h" without use. diff --git a/MODULES.html.sh b/MODULES.html.sh --- a/MODULES.html.sh +++ b/MODULES.html.sh @@ -2558,6 +2558,7 @@ func_module freading func_module freadptr func_module freadseek + func_module freopen-safer func_module fwritable func_module fwriting func_module getpass diff --git a/doc/posix-functions/freopen.texi b/doc/posix-functions/freopen.texi --- a/doc/posix-functions/freopen.texi +++ b/doc/posix-functions/freopen.texi @@ -23,4 +23,9 @@ and (without the slash) names a nonexistent file or a file that is not a directory, on some platforms: HP-UX 11.00, Solaris 9, Irix 5.3. +@item +Applications should not assume that @code{fileno(f)} will be the same +before and after a call to @code{freopen(name,mode,f)}. However, the +module freopen-safer can at least protect @code{stdin}, @code{stdout}, +and @code{stderr}. @end itemize diff --git a/doc/posix-functions/stderr.texi b/doc/posix-functions/stderr.texi --- a/doc/posix-functions/stderr.texi +++ b/doc/posix-functions/stderr.texi @@ -17,4 +17,11 @@ One workaround is to use freopen(NULL, ``r+'', stderr) on Cygwin 1.5.21 or newer. Another is to use the gnulib ftello module and do ftello(stderr). +@item +POSIX states that a setuid application can guarantee that fd 2 is +open, but some systems guarantee this even for non-setuid programs. +If an application is executed with fd 2 closed, use of @code{stderr} +can affect an unrelated file that happened to be assigned to fd 2. +The gnulib *-safer modules may be used to guarantee that fd 2 stays +reserved for @code{stderr}. @end itemize diff --git a/doc/posix-functions/stdin.texi b/doc/posix-functions/stdin.texi --- a/doc/posix-functions/stdin.texi +++ b/doc/posix-functions/stdin.texi @@ -17,4 +17,11 @@ One workaround is to use freopen(NULL, ``r'', stdin) on Cygwin 1.5.21 or newer. Another is to use the gnulib ftello module and do ftello(stdin). +@item +POSIX states that a setuid application can guarantee that fd 0 is +open, but some systems guarantee this even for non-setuid programs. +If an application is executed with fd 0 closed, use of @code{stdin} +can affect an unrelated file that happened to be assigned to fd 0. +The gnulib *-safer modules may be used to guarantee that fd 0 stays +reserved for @code{stdin}. @end itemize diff --git a/doc/posix-functions/stdout.texi b/doc/posix-functions/stdout.texi --- a/doc/posix-functions/stdout.texi +++ b/doc/posix-functions/stdout.texi @@ -17,4 +17,11 @@ One workaround is to use freopen(NULL, ``w'', stdout) on Cygwin 1.5.21 or newer. Another is to use the gnulib ftello module and do ftello(stdout). +@item +POSIX states that a setuid application can guarantee that fd 1 is +open, but some systems guarantee this even for non-setuid programs. +If an application is executed with fd 1 closed, use of @code{stdout} +can affect an unrelated file that happened to be assigned to fd 1. +The gnulib *-safer modules may be used to guarantee that fd 1 stays +reserved for @code{stdout}. @end itemize diff --git a/lib/freopen-safer.c b/lib/freopen-safer.c new file mode 100644 --- /dev/null +++ b/lib/freopen-safer.c @@ -0,0 +1,103 @@ +/* Invoke freopen, but avoid some glitches. + + 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 "stdio-safer.h" + +#include +#include +#include +#include + +/* Guarantee that FD is open; all smaller FDs must already be open. + Return true if successful. */ +static bool +protect_fd (int fd) +{ + int value = open ("/dev/null", O_RDONLY); + if (value != fd) + { + if (0 <= value) + { + close (value); + errno = EBADF; /* Unexpected; this is as good as anything else. */ + } + return false; + } + return true; +} + +/* Like freopen, but guarantee that reopening stdin, stdout, or stderr + preserves the invariant that STDxxx_FILENO==fileno(stdxxx), and + that no other stream will interfere with the standard streams. + This is necessary because most freopen implementations will change + the associated fd of a stream to the lowest available slot. */ + +FILE * +freopen_safer (char const *name, char const *mode, FILE *f) +{ + /* Unfortunately, we cannot use the fopen_safer approach of using + fdopen (dup_safer (fileno (freopen (cmd, mode, f)))), because we + need to return f itself. The implementation of freopen(NULL,m,f) + is system-dependent, so the best we can do is guarantee that all + lower-valued standard fds are open prior to the freopen call, + even though this puts more pressure on open fds. */ + bool protect_in = false; + bool protect_out = false; + bool protect_err = false; + int saved_errno; + + switch (fileno (f)) + { + default: /* -1 or not a standard stream. */ + if (dup2 (STDERR_FILENO, STDERR_FILENO) != STDERR_FILENO) + protect_err = true; + /* fall through */ + case STDERR_FILENO: + if (dup2 (STDOUT_FILENO, STDOUT_FILENO) != STDOUT_FILENO) + protect_out = true; + /* fall through */ + case STDOUT_FILENO: + if (dup2 (STDIN_FILENO, STDIN_FILENO) != STDIN_FILENO) + protect_in = true; + /* fall through */ + case STDIN_FILENO: + /* Nothing left to protect. */ + break; + } + if (protect_in && !protect_fd (STDIN_FILENO)) + f = NULL; + else if (protect_out && !protect_fd (STDOUT_FILENO)) + f = NULL; + else if (protect_err && !protect_fd (STDERR_FILENO)) + f = NULL; + else + f = freopen (name, mode, f); + saved_errno = errno; + if (protect_err) + close (STDERR_FILENO); + if (protect_out) + close (STDOUT_FILENO); + if (protect_in) + close (STDIN_FILENO); + if (!f) + errno = saved_errno; + return f; +} diff --git a/lib/stdio--.h b/lib/stdio--.h --- a/lib/stdio--.h +++ b/lib/stdio--.h @@ -25,6 +25,11 @@ # define fopen fopen_safer #endif +#if GNULIB_FREOPEN_SAFER +# undef freopen +# define freopen freopen_safer +#endif + #if GNULIB_TMPFILE_SAFER # undef tmpfile # define tmpfile tmpfile_safer diff --git a/lib/stdio-safer.h b/lib/stdio-safer.h --- a/lib/stdio-safer.h +++ b/lib/stdio-safer.h @@ -19,6 +19,18 @@ #include +#if GNULIB_FOPEN_SAFER FILE *fopen_safer (char const *, char const *); +#endif + +#if GNULIB_FREOPEN_SAFER +FILE *freopen_safer (char const *, char const *, FILE *); +#endif + +#if GNULIB_POPEN_SAFER FILE *popen_safer (char const *, char const *); +#endif + +#if GNULIB_TMPFILE_SAFER FILE *tmpfile_safer (void); +#endif diff --git a/m4/stdio-safer.m4 b/m4/stdio-safer.m4 --- a/m4/stdio-safer.m4 +++ b/m4/stdio-safer.m4 @@ -1,4 +1,4 @@ -#serial 11 +#serial 12 dnl Copyright (C) 2002, 2005-2007, 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, @@ -9,6 +9,11 @@ AC_LIBOBJ([fopen-safer]) ]) +AC_DEFUN([gl_FREOPEN_SAFER], +[ + AC_LIBOBJ([freopen-safer]) +]) + AC_DEFUN([gl_POPEN_SAFER], [ AC_LIBOBJ([popen-safer]) diff --git a/modules/freopen-safer b/modules/freopen-safer new file mode 100644 --- /dev/null +++ b/modules/freopen-safer @@ -0,0 +1,29 @@ +Description: +freopen function that avoids clobbering std{in,out,err}. + +Files: +lib/stdio--.h +lib/stdio-safer.h +lib/freopen-safer.c +m4/stdio-safer.m4 + +Depends-on: +dup2 +freopen +open +stdbool + +configure.ac: +gl_FREOPEN_SAFER +gl_MODULE_INDICATOR([freopen-safer]) + +Makefile.am: + +Include: +"stdio-safer.h" + +License: +GPL + +Maintainer: +Eric Blake diff --git a/modules/freopen-safer-tests b/modules/freopen-safer-tests new file mode 100644 --- /dev/null +++ b/modules/freopen-safer-tests @@ -0,0 +1,10 @@ +Files: +tests/test-freopen-safer.c + +Depends-on: + +configure.ac: + +Makefile.am: +TESTS += test-freopen-safer +check_PROGRAMS += test-freopen-safer diff --git a/tests/test-freopen-safer.c b/tests/test-freopen-safer.c new file mode 100644 --- /dev/null +++ b/tests/test-freopen-safer.c @@ -0,0 +1,107 @@ +/* Test of reopening a stream. + 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 , 2009. */ + +#include + +/* Specification. */ +#include "stdio--.h" + +/* Helpers. */ +#include +#include + +/* This test intentionally closes stderr. So, we arrange to have fd 10 + (outside the range of interesting fd's during the test) set up to + duplicate the original stderr. */ + +#define BACKUP_STDERR_FILENO 10 +static FILE *myerr; + +#define ASSERT(expr) \ + do \ + { \ + if (!(expr)) \ + { \ + fprintf (myerr, "%s:%d: assertion failed\n", __FILE__, __LINE__); \ + fflush (myerr); \ + abort (); \ + } \ + } \ + while (0) + +int +main (int argc, char **argv) +{ + FILE *fp; + + /* We close fd 2 later, so save it in fd 10. */ + if (dup2 (STDERR_FILENO, BACKUP_STDERR_FILENO) != BACKUP_STDERR_FILENO + || (myerr = fdopen (BACKUP_STDERR_FILENO, "w")) == NULL) + return 2; + + { + FILE *tmp; + ASSERT (tmp = fopen ("/dev/null", "r")); + ASSERT (STDERR_FILENO < fileno (tmp)); + ASSERT (fp = fopen ("/dev/null", "w")); + ASSERT (fileno (tmp) < fileno (fp)); + ASSERT (fclose (tmp) == 0); + } + + /* Gap in fds. */ + ASSERT (freopen ("/dev/null", "r+", fp) == fp); + ASSERT (STDERR_FILENO < fileno (fp)); + + ASSERT (freopen ("/dev/null", "r", stdin) == stdin); + ASSERT (STDIN_FILENO == fileno (stdin)); + + ASSERT (freopen ("/dev/null", "w", stdout) == stdout); + ASSERT (STDOUT_FILENO == fileno (stdout)); + + ASSERT (freopen ("/dev/null", "w", stderr) == stderr); + ASSERT (STDERR_FILENO == fileno (stderr)); + + /* fd 0 closed. */ + ASSERT (close (STDIN_FILENO) == 0); + + ASSERT (freopen ("/dev/null", "w", stdout) == stdout); + ASSERT (STDOUT_FILENO == fileno (stdout)); + + ASSERT (freopen ("/dev/null", "w", stderr) == stderr); + ASSERT (STDERR_FILENO == fileno (stderr)); + + ASSERT (freopen ("/dev/null", "a", fp) == fp); + ASSERT (STDERR_FILENO < fileno (fp)); + + /* fd 1 closed. */ + ASSERT (close (STDOUT_FILENO) == 0); + + ASSERT (freopen ("/dev/null", "w", stderr) == stderr); + ASSERT (STDERR_FILENO == fileno (stderr)); + + ASSERT (freopen ("/dev/null", "a+", fp) == fp); + ASSERT (STDERR_FILENO < fileno (fp)); + + /* fd 2 closed. */ + ASSERT (close (STDERR_FILENO) == 0); + + ASSERT (freopen ("/dev/null", "w+", fp) == fp); + ASSERT (STDERR_FILENO < fileno (fp)); + + return 0; +}