bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[bug-gnulib] Re: robust chdir and openat (works with names longer than P


From: Jim Meyering
Subject: [bug-gnulib] Re: robust chdir and openat (works with names longer than PATH_MAX)
Date: Sun, 28 Nov 2004 22:31:23 +0100

Jim Meyering <address@hidden> wrote:
> I've written a chdir wrapper that works even for directory
> names longer than PATH_MAX.
> ...

For your convenience, here is chdir.c:

/* provide a chdir function that tries not to fail due to ENAMETOOLONG
   Copyright (C) 2004 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 2, 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, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

/* written by Jim Meyering */

#include <config.h>

#include "chdir.h"

/* Disable the definition of chdir to rpl_chdir (from chdir.h) in this
   file.  Otherwise, we'd get conflicting prototypes for rpl_chdir on
   most systems.  */
#undef chdir

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <assert.h>
#include <limits.h>

#include "mempcpy.h"
#include "openat.h"

#ifndef MIN
# define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif

#ifndef PATH_MAX
# ifdef MAXPATHLEN
#  define PATH_MAX MAXPATHLEN
# else
#  error "use this module only if your system defines PATH_MAX"
# endif
#endif

/* FIXME: this use of `MIN' is our sole concession to arbitrary limitations.
   If, for some system, PATH_MAX is larger than 8191 and you call
   rpl_chdir with a directory name that is longer than PATH_MAX,
   yet that contains a single component that is more than 8191 bytes
   long, then this function will fail.  */
#define MAX_COMPONENT_LENGTH MIN (PATH_MAX - 1, 8 * 1024)

struct cd_buf
{
  /* FIXME maybe allocate this via malloc, rather than using the stack.
     But that would be the sole use of malloc.  Is it worth it to
     let chdir fail due to a low-memory condition?
     But when using malloc, and assuming we remove the `concession'
     above, we'll still have to avoid allocating 2^31 bytes on
     systems that define PATH_MAX to very large number.
     Ideally, we'd allocate enough to deal with most names, and
     dynamically increase the buffer size only necessary.  */
  char buffer[MAX_COMPONENT_LENGTH + 1];
  char *avail;
  int fd;
};

static void
cdb_init (struct cd_buf *cdb)
{
  cdb->avail = cdb->buffer;
  cdb->fd = AT_FDCWD;
}

static inline bool
cdb_empty (struct cd_buf const *cdb)
{
  return cdb->avail == cdb->buffer;
}

static inline int
cdb_fchdir (struct cd_buf const *cdb)
{
  return fchdir (cdb->fd);
}

static int
cdb_advance_fd (struct cd_buf *cdb, char const *dir)
{
  int new_fd = openat (cdb->fd, dir, O_RDONLY | O_DIRECTORY);
  if (new_fd < 0)
    {
      new_fd = openat (cdb->fd, dir, O_WRONLY | O_DIRECTORY);
      if (new_fd < 0)
        return -1;
    }

  if (cdb->fd != AT_FDCWD)
    close (cdb->fd);
  cdb->fd = new_fd;

  return 0;
}

static int
cdb_flush (struct cd_buf *cdb)
{
  if (cdb_empty (cdb))
    return 0;

  cdb->avail[0] = '\0';
  if (cdb_advance_fd (cdb, cdb->buffer) != 0)
    return -1;

  cdb->avail = cdb->buffer;

  return 0;
}

static void
cdb_free (struct cd_buf *cdb)
{
  if (0 <= cdb->fd && close (cdb->fd) != 0)
    abort ();
}

static int
cdb_append (struct cd_buf *cdb, char const *s, size_t len)
{
  char const *end = cdb->buffer + sizeof cdb->buffer;

  /* Insert a slash separator if there is a preceding byte
     and it's not a slash.  */
  bool need_slash = (cdb->buffer < cdb->avail && cdb->avail[-1] != '/');
  size_t n_free;

  if (sizeof cdb->buffer < len + 1)
    {
      /* This single component is too long.  */
      errno = ENAMETOOLONG;
      return -1;
    }

  /* See if there's enough room for the `/', the new component and
     a trailing NUL.  */
  n_free = end - cdb->avail;
  if (n_free < need_slash + len + 1)
    {
      if (cdb_flush (cdb) != 0)
        return -1;
      need_slash = false;
    }

  if (need_slash)
    *(cdb->avail)++ = '/';

  cdb->avail = mempcpy (cdb->avail, s, len);
  return 0;
}

/* This is a wrapper around chdir that works even on PATH_MAX-limited
   systems.  It handles an arbitrarily long directory name by extracting
   and processing manageable portions of the name.  On systems without
   the openat syscall, this means changing the working directory to
   more and more `distant' points along the long directory name and
   then restoring the working directory.
   If any of those attempts to change or restore the working directory
   fails, this function exits nonzero.

   Note that this function may still fail with errno == ENAMETOOLONG,
   but only if the specified directory name contains a component that
   is long enough to provoke such a failure all by itself (e.g. if the
   component is longer than PATH_MAX on systems that define PATH_MAX).  */

int
rpl_chdir (char const *dir)
{
  int e = chdir (dir);
  if (e == 0 || errno != ENAMETOOLONG)
    return e;

  {
    size_t len = strlen (dir);
    char const *dir_end = dir + len;
    char const *d;
    struct cd_buf cdb;

    cdb_init (&cdb);

    /* If DIR is the empty string, then the chdir above
       must have failed and set errno to ENOENT.  */
    assert (0 < len);

    if (*dir == '/')
      {
        /* Names starting with exactly two slashes followed by at least
           one non-slash are special --
           for example, in some environments //Hostname/file may
           denote a file on a different host.
           Preserve those two leading slashes.  Treat all other
           sequences of slashes like a single one.  */
        if (3 <= len && dir[1] == '/' && dir[2] != '/')
          {
            size_t name_len = 1 + strcspn (dir + 3, "/");
            if (cdb_append (&cdb, dir, 2 + name_len) != 0)
              goto Fail;
            /* Advance D to next slash or to end of string. */
            d = dir + 2 + name_len;
            assert (*d == '/' || *d == '\0');
          }
        else
          {
            if (cdb_append (&cdb, "/", 1) != 0)
              goto Fail;
            d = dir + 1;
          }
      }
    else
      {
        d = dir;
      }

    while (1)
      {
        /* Skip any slashes to find start of next component --
           or the end of DIR. */
        char const *start = d + strspn (d, "/");
        if (*start == '\0')
          {
            if (cdb_flush (&cdb) != 0)
              goto Fail;
            break;
          }
        /* If the remaining portion is no longer than PATH_MAX, then
           flush anything that is buffered and do the rest in one chunk.  */
        if (dir_end - start <= PATH_MAX)
          {
            if (cdb_flush (&cdb) != 0
                || cdb_advance_fd (&cdb, start) != 0)
              goto Fail;
            break;
          }

        /* FIXME: if performance of strcspn is an issue,
           use a little wrapper around memchr.  */
        len = strcspn (start, "/");
        d = start + len;
        if (cdb_append (&cdb, start, len) != 0)
          goto Fail;
      }

    if (cdb_fchdir (&cdb) != 0)
      goto Fail;

    cdb_free (&cdb);
    return 0;

   Fail:
    {
      int saved_errno = errno;
      cdb_free (&cdb);
      errno = saved_errno;
      return -1;
    }
  }
}

#if TEST_CHDIR

# include <stdio.h>
# include "closeout.h"
# include "error.h"

char *program_name;

int
main (int argc, char *argv[])
{
  char *line = NULL;
  size_t n = 0;
  int len;

  program_name = argv[0];
  atexit (close_stdout);

  len = getline (&line, &n, stdin);
  if (len < 0)
    {
      int saved_errno = errno;
      if (feof (stdin))
        exit (0);

      error (EXIT_FAILURE, saved_errno, _("reading standard input"));
    }
  else if (len == 0)
    exit (0);

  if (line[len-1] == '\n')
    line[len-1] = '\0';

  if (rpl_chdir (line) != 0)
    error (EXIT_FAILURE, errno, _("chdir failed: %s"), line);

  {
    /* Using `pwd' here makes sense only if it is a robust implementation,
       like the one in coreutils after the 2004-04-19 changes.  */
    char const *cmd = "pwd";
    execlp (cmd, (char *) NULL);
    error (EXIT_FAILURE, errno, "%s", cmd);
  }

  /* not reached */
  abort ();
}
#endif

/*
Local Variables:
compile-command: "gcc -DTEST_CHDIR=1 -DHAVE_CONFIG_H -I.. -g -O -W -Wall 
chdir.c libfetish.a"
End:
*/




reply via email to

[Prev in Thread] Current Thread [Next in Thread]