[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: xreadlink: Rewrite as a simple mreadlink wrapper.
From: |
Jim Meyering |
Subject: |
Re: xreadlink: Rewrite as a simple mreadlink wrapper. |
Date: |
Mon, 03 Sep 2007 01:15:07 +0200 |
Bruno Haible <address@hidden> wrote:
> Hello Jim,
>
>> as far as I can see, no one even uses xreadlink any more, so it's not urgent.
>
> The 'relocatable' facility uses xreadlink.
>
>> However, your point about the "a" vs. "m" prefix is valid,
>> and I've just renamed mreadlink-with-size.
>
> Thanks. I've got an 'areadlink' module ready now, but I would like to
> get the malloc/realloc situation cleared up before I present it.
Hi Bruno,
Since they may be relevant (you can implement areadlink
via a wrapper around areadlinkat_with_size -- though may not
want to), here are some new modules as part of a patch
for coreutils that's nearly complete:
>From 1679a208de82815519a2b4e66592da68dfd11360 Mon Sep 17 00:00:00 2001
From: Jim Meyering <address@hidden>
Date: Fri, 3 Aug 2007 18:44:54 +0200
Subject: [PATCH] copy.c: handle dangling destination symlinks safely
Signed-off-by: Jim Meyering <address@hidden>
---
.x-sc_prohibit_strcmp | 3 +-
ChangeLog | 21 +++
bootstrap.conf | 4 +-
gl/lib/areadlinkat-with-size.c | 113 ++++++++++++++++
gl/lib/areadlinkat.h | 3 +
gl/lib/resolve-symlink.c | 261 ++++++++++++++++++++++++++++++++++++++
gl/lib/resolve-symlink.h | 2 +
gl/modules/areadlinkat | 27 ++++
gl/modules/resolve-symlink | 27 ++++
gl/modules/resolve-symlink-tests | 13 ++
gl/tests/test-resolve-symlink.c | 117 +++++++++++++++++
gl/tests/test-resolve-symlink.sh | 67 ++++++++++
src/copy.c | 35 ++++--
13 files changed, 681 insertions(+), 12 deletions(-)
create mode 100644 gl/lib/areadlinkat-with-size.c
create mode 100644 gl/lib/areadlinkat.h
create mode 100644 gl/lib/resolve-symlink.c
create mode 100644 gl/lib/resolve-symlink.h
create mode 100644 gl/modules/areadlinkat
create mode 100644 gl/modules/resolve-symlink
create mode 100644 gl/modules/resolve-symlink-tests
create mode 100644 gl/tests/test-resolve-symlink.c
create mode 100755 gl/tests/test-resolve-symlink.sh
diff --git a/.x-sc_prohibit_strcmp b/.x-sc_prohibit_strcmp
index fdceaf0..e8dfba1 100644
--- a/.x-sc_prohibit_strcmp
+++ b/.x-sc_prohibit_strcmp
@@ -1,2 +1,3 @@
-^src/system\.h
+^src/system\.h$
ChangeLog
+^gl/tests/test-resolve-symlink\.c$
diff --git a/ChangeLog b/ChangeLog
index 7153fd6..bec3197 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -401,6 +401,27 @@
* src/system.h (opt_str_storage): New static var.
(OPT_STR, LONG_OPT_STR, OPT_STR_INIT): New macros.
+2007-08-03 Jim Meyering <address@hidden>
+
+ copy.c: handle dangling destination symlinks safely
+
+ * src/copy.c: Include "resolve-symlink.h", not "canonicalize.h".
+ (copy_reg): Safely handle the unusual case of copying through a
+ dangling destination symlink: i.e., by detecting when it would
+ be risky and failing in that case.
+ Call resolve_symlink to get enough information (fd and ent_name)
+ to open the file safely, then open/create it with openat.
+ * bootstrap.conf (gnulib_modules): Add resolve-symlink.
+ * gl/modules/resolve-symlink: New file.
+ * gl/modules/areadlinkat: New file.
+ * gl/lib/areadlinkat-with-size.c: New file.
+ * gl/lib/areadlinkat.h: New file.
+ * gl/lib/resolve-symlink.c: New file.
+ * gl/lib/resolve-symlink.h: New file.
+ * gl/modules/resolve-symlink-tests: New file.
+ * gl/tests/test-resolve-symlink.c: New file.
+ * gl/tests/test-resolve-symlink.sh: New file.
+
2007-08-02 Jim Meyering <address@hidden>
Adjust one more test to accommodate the recent fts change.
diff --git a/bootstrap.conf b/bootstrap.conf
index 93bada9..7651c16 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -64,7 +64,9 @@ gnulib_modules="
mountlist mpsort obstack pathmax perl physmem posixtm posixver putenv
quote quotearg raise readlink areadlink-with-size readtokens
readtokens0 readutmp
- realloc regex rename-dest-slash rmdir rmdir-errno
+ realloc regex rename-dest-slash
+ resolve-symlink
+ rmdir rmdir-errno
root-dev-ino
rpmatch
safe-read same
diff --git a/gl/lib/areadlinkat-with-size.c b/gl/lib/areadlinkat-with-size.c
new file mode 100644
index 0000000..1a76426
--- /dev/null
+++ b/gl/lib/areadlinkat-with-size.c
@@ -0,0 +1,113 @@
+/* readlinkat wrapper: return symlink name in malloc'd storage
+
+ Copyright (C) 2007 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 <http://www.gnu.org/licenses/>. */
+
+/* Written by Jim Meyering <address@hidden>. */
+
+#include <config.h>
+
+#include "areadlinkat.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifndef SIZE_MAX
+# define SIZE_MAX ((size_t) -1)
+#endif
+#ifndef SSIZE_MAX
+# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
+#endif
+
+/* SYMLINK_MAX is used only for an initial memory-allocation sanity
+ check, so it's OK to guess too small on hosts where there is no
+ arbitrary limit to symbolic link length. */
+#ifndef SYMLINK_MAX
+# define SYMLINK_MAX 1024
+#endif
+
+#define MAXSIZE (SIZE_MAX < SSIZE_MAX ? SIZE_MAX : SSIZE_MAX)
+
+/* Use readlinkat to get FILE's link name into malloc'd storage.
+ SIZE is a hint as to how long the string is expected to be;
+ typically it is taken from st_size. It need not be correct.
+ Return a pointer to that NUL-terminated string in malloc'd storage.
+ If readlinkat or malloc fails, or if the link value is longer than
+ SSIZE_MAX, return NULL (caller may use errno to diagnose). */
+
+char *
+areadlinkat_with_size (int fd, char const *file, size_t size)
+{
+ /* Some buggy file systems report garbage in st_size. Defend
+ against them by ignoring outlandish st_size values in the initial
+ memory allocation. */
+ size_t symlink_max = SYMLINK_MAX;
+ size_t INITIAL_LIMIT_BOUND = 8 * 1024;
+ size_t initial_limit = (symlink_max < INITIAL_LIMIT_BOUND
+ ? symlink_max + 1
+ : INITIAL_LIMIT_BOUND);
+
+ /* The initial buffer size for the link value. */
+ size_t buf_size = size < initial_limit ? size + 1 : initial_limit;
+
+ while (1)
+ {
+ ssize_t r;
+ size_t link_length;
+ char *buffer = malloc (buf_size);
+
+ if (buffer == NULL)
+ return NULL;
+ r = readlinkat (fd, file, buffer, buf_size);
+
+ /* On AIX 5L v5.3 and HP-UX 11i v2 04/09, emulated readlinkat
+ returns -1 with errno == ERANGE if the buffer is too small. */
+ if (r < 0 && errno != ERANGE)
+ {
+ int saved_errno = errno;
+ free (buffer);
+ errno = saved_errno;
+ return NULL;
+ }
+
+ link_length = r;
+ if (link_length < buf_size)
+ {
+ buffer[link_length] = 0;
+ return buffer;
+ }
+
+ free (buffer);
+ if (buf_size <= MAXSIZE / 2)
+ buf_size *= 2;
+ else if (buf_size < MAXSIZE)
+ buf_size = MAXSIZE;
+ else
+ {
+ errno = ENOMEM;
+ return NULL;
+ }
+ }
+}
+
+/*
+Local Variables:
+indent-tabs-mode: nil
+End:
+*/
diff --git a/gl/lib/areadlinkat.h b/gl/lib/areadlinkat.h
new file mode 100644
index 0000000..3ce29b7
--- /dev/null
+++ b/gl/lib/areadlinkat.h
@@ -0,0 +1,3 @@
+#include <stddef.h>
+extern char *areadlinkat_with_size (int fd, char const *filename,
+ size_t est_len);
diff --git a/gl/lib/resolve-symlink.c b/gl/lib/resolve-symlink.c
new file mode 100644
index 0000000..9a4febd
--- /dev/null
+++ b/gl/lib/resolve-symlink.c
@@ -0,0 +1,261 @@
+/* Resolve a symbolic link, safely.
+ Copyright (C) 2007 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 <http://www.gnu.org/licenses/>. */
+
+/* Written by Jim Meyering. */
+
+#include <config.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "cycle-check.h"
+#include "dirname.h"
+#include "filenamecat.h"
+#include "areadlinkat.h"
+
+/* An estimate of symlink length; used as a hint to areadlinkat_with_size. */
+enum { SYMLINK_LENGTH_EST = 63 };
+
+static inline bool
+legit_readlink_errno (int errno_val)
+{
+ /* ENOENT is for when FILE is a dangling symlink,
+ EINVAL is for when FILE is not a symlink. */
+ return errno_val == ENOENT || errno_val == EINVAL;
+}
+
+/* Return true if DIR_FD is owned by the effective user ID, and not writable
+ by "other". Ideally, what we want here is to ensure that the directory
+ behind *NEW_FD is writable only by us (wrt existing intermediate
+ symlinks as well as the destination of the final, dangling one),
+ so the sticky bit is not relevant. This is just an approximation,
+ since it ignores group permissions, but since it does ensure you're
+ the owner, it is your responsibility not to grant group write access
+ if other members are not trustworthy. */
+static bool
+safely_readable_dir (int dir_fd)
+{
+ struct stat sb;
+
+ /* Stop the traversal if the directory we've just opened is not owned by
+ the effective user ID. Ideally, what we want here is to ensure that
+ the directory behind *NEW_FD is writable only by us, but it's not
+ feasible to check that. */
+ return (fstat (dir_fd, &sb) == 0
+ && geteuid () == sb.st_uid
+ && ! (sb.st_mode & S_IWOTH));
+}
+
+/* Given the dirname/basename parts of a file name that specifies a symlink,
+ take one step in the follow-the-symlink-chain process.
+
+ Inputs: file descriptor *FD, open on a directory (or AT_FDCWD),
+ an FD-relative directory name, DIR, ENT the base name of the
+ probable-symlink, and CWD_FULL_NAME, the full name of the directory
+ to which DIR is relative (initially this is cwd),
+ to be used only if openat fails.
+
+ This function always closes an open (i.e., non-AT_FDCWD) *FD.
+
+ Get the symlink value from "DIR/ENT" via openat+readlinkat or via
+ readlink "CWD_FULL_NAME/DIR/ENT". In either case, update *FD accordingly
+ (to new dir fd in the first case, to AT_FDCWD in the latter).
+ Upon noting a successful return from this function, the caller will
+ probably want to form the next directory name, CWD_FULL_NAME/DIR.
+ Return the symlink value in malloc'd storage, or NULL upon error. */
+
+static char *
+traverse_symlink (int *fd, char const *dir, char const *ent,
+ char const *cwd_full_name)
+{
+ int new_fd = openat (*fd, dir, O_RDONLY | O_DIRECTORY);
+
+ if (*fd != AT_FDCWD)
+ close (*fd);
+
+ if (0 <= new_fd)
+ {
+ char *val;
+
+ /* Stop the traversal if the directory we've just opened is not "safe"
+ enough, i.e., if someone else could add or modify the symlink we're
+ about to read. */
+ if (! safely_readable_dir (new_fd))
+ {
+ close (new_fd);
+ errno = EPERM;
+ return NULL;
+ }
+
+ val = areadlinkat_with_size (new_fd, ent, SYMLINK_LENGTH_EST);
+ if (val || legit_readlink_errno (errno))
+ {
+ *fd = new_fd;
+ return val;
+ }
+ close (new_fd);
+ }
+
+ /* We reach this point only in the relatively unusual event that openat
+ fails or that readlinkat fails with an unusual errno value. */
+ *fd = AT_FDCWD;
+
+ {
+ /* Form the concatenation: CWD_FULL_NAME/DIR/ENT. */
+ /* FIXME: is it possible for dir to start with a slash? */
+ char *t = mfile_name_concat (cwd_full_name, dir, NULL);
+ if (t == NULL)
+ return NULL;
+ char *p = mfile_name_concat (t, ent, NULL);
+ free (t);
+ if (p == NULL)
+ return NULL;
+ char *val = areadlinkat_with_size (AT_FDCWD, p, SYMLINK_LENGTH_EST);
+ if (val == NULL)
+ {
+ int saved_errno = errno;
+ free (p);
+ errno = saved_errno;
+ return NULL;
+ }
+
+ free (p);
+ return val;
+ }
+}
+
+static inline bool
+is_dot (char const *filename)
+{
+ return *filename == '.' && filename[1] == '\0';
+}
+
+/* Given a dangling symlink, S, set *DIR_FD and *ENT_NAME so that
+ openat (DIR_FD, ENT_NAME, flags | O_EXCL) creates the same file that
+ open (S, flags) would create. Using O_EXCL is safer. Also, set
+ *PARENT_FULL_NAME to the full name (in malloc'd memory) of the
+ directory containing the target of S. I.e., in shell parlance,
+ set *PARENT_FULL_NAME to $(dirname $(readlink -m S)).
+
+ As for PARENT_FULL_NAME, given that resolving S means following
+ a sequence of more than one symlink, ...
+ if the final symlink value is an absolute name, that is the result.
+ Otherwise, if all links are relative, the result is the concatenation
+ of relative dirname values with the final link value.
+ Otherwise, it is the concatenation of dirname (last_absolute_name) and
+ the following relative dirname values with the final link value.
+*/
+int
+resolve_symlink (char const *slink, int *dir_fd,
+ char **ent_name, char **parent_full_name)
+{
+ int fd = AT_FDCWD;
+ /* FIXME: is it ok to use "" here? */
+ char *next_parent = strdup ("");
+ if (next_parent == NULL)
+ return -1;
+
+ char *s = strdup (slink);
+ if (s == NULL)
+ {
+ free (next_parent);
+ return -1;
+ }
+
+ size_t n = 0;
+ struct cycle_check_state cycle_check_state;
+ cycle_check_init (&cycle_check_state);
+
+ while (true)
+ {
+ char *dir = dir_name (s);
+ if (dir == NULL)
+ goto fail;
+ char *base = last_component (s);
+ if (*base == '\0')
+ goto fail;
+
+ char *val = traverse_symlink (&fd, dir, base, next_parent);
+ if (val == NULL && ! legit_readlink_errno (errno))
+ {
+ fail:;
+ int saved_errno = errno;
+ free (next_parent);
+ free (s);
+ free (dir);
+ errno = saved_errno;
+ return -1;
+ }
+
+ if (IS_ABSOLUTE_FILE_NAME (s))
+ {
+ next_parent = dir_name (s);
+ }
+ else if (!is_dot (dir))
+ {
+ /* Append DIR to NEXT_PARENT -- as long as it's not ".". */
+ char *p = mfile_name_concat (next_parent, dir, NULL);
+ int saved_errno = errno;
+ free (next_parent);
+ errno = saved_errno;
+ next_parent = p;
+ }
+
+ if (val == NULL && legit_readlink_errno (errno))
+ {
+ char *b = strdup (base);
+ if (b == NULL)
+ goto fail;
+ *ent_name = b;
+ free (dir);
+ free (s);
+ *dir_fd = fd;
+ /* Convert "" to ".". */
+ if (*next_parent == '\0')
+ {
+ free (next_parent);
+ next_parent = strdup (".");
+ }
+ *parent_full_name = next_parent;
+ return 0;
+ }
+
+ /* Save fstatat overhead until after we've traversed a few symlinks. */
+ if (5 < ++n)
+ {
+ struct stat sb;
+ if (fstatat (fd, *ent_name, &sb, AT_SYMLINK_NOFOLLOW) == 0
+ && cycle_check (&cycle_check_state, &sb))
+ {
+ errno = ELOOP;
+ return -1;
+ }
+ }
+
+ free (s);
+ free (dir);
+
+ s = val;
+ }
+}
+
+/*
+Local Variables:
+indent-tabs-mode: nil
+End:
+*/
diff --git a/gl/lib/resolve-symlink.h b/gl/lib/resolve-symlink.h
new file mode 100644
index 0000000..c552df7
--- /dev/null
+++ b/gl/lib/resolve-symlink.h
@@ -0,0 +1,2 @@
+extern int resolve_symlink (char const *symlink, int *dir_fd,
+ char **ent_name, char **full_name);
diff --git a/gl/modules/areadlinkat b/gl/modules/areadlinkat
new file mode 100644
index 0000000..fdaa329
--- /dev/null
+++ b/gl/modules/areadlinkat
@@ -0,0 +1,27 @@
+Description:
+Resolve symbolic links without size limitation.
+
+Files:
+lib/areadlinkat.h
+lib/areadlinkat-with-size.c
+
+Depends-on:
+openat
+readlink
+ssize_t
+unistd
+xalloc
+
+configure.ac:
+
+Makefile.am:
+lib_SOURCES += areadlinkat-with-size.c
+
+Include:
+"areadlink.h"
+
+License:
+LGPL
+
+Maintainer:
+Jim Meyering
diff --git a/gl/modules/resolve-symlink b/gl/modules/resolve-symlink
new file mode 100644
index 0000000..ba4e722
--- /dev/null
+++ b/gl/modules/resolve-symlink
@@ -0,0 +1,27 @@
+Description:
+Resolve a symbolic link, safely.
+
+Files:
+lib/resolve-symlink.h
+lib/resolve-symlink.c
+
+Depends-on:
+cycle-check
+dirname
+filenamecat
+areadlinkat
+strdup
+
+configure.ac:
+
+Makefile.am:
+lib_SOURCES += resolve-symlink.c
+
+Include:
+"resolve-symlink.h"
+
+License:
+GPL
+
+Maintainer:
+Jim Meyering
diff --git a/gl/modules/resolve-symlink-tests b/gl/modules/resolve-symlink-tests
new file mode 100644
index 0000000..001cd53
--- /dev/null
+++ b/gl/modules/resolve-symlink-tests
@@ -0,0 +1,13 @@
+Files:
+tests/test-resolve-symlink.c
+tests/test-resolve-symlink.sh
+
+Depends-on:
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-resolve-symlink.sh
+TESTS_ENVIRONMENT += EXEEXT='@EXEEXT@'
+check_PROGRAMS += test-resolve-symlink
+EXTRA_DIST += test-resolve-symlink.sh
diff --git a/gl/tests/test-resolve-symlink.c b/gl/tests/test-resolve-symlink.c
new file mode 100644
index 0000000..c77671c
--- /dev/null
+++ b/gl/tests/test-resolve-symlink.c
@@ -0,0 +1,117 @@
+#include <config.h>
+#include <stdio.h>
+#include <assert.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include "error.h"
+#include "filenamecat.h"
+#include "resolve-symlink.h"
+#include "same-inode.h"
+#include "xgetcwd.h"
+
+#undef _
+#define _(msg) msg
+
+#define STREQ(a, b) (strcmp (a, b) == 0)
+
+static void
+check_one (char const *s)
+{
+ int fd;
+ char *ent_name;
+ char *parent_dir;
+ int err = resolve_symlink (s, &fd, &ent_name, &parent_dir);
+ if (err == 0)
+ {
+ int e;
+ struct stat sb1;
+ struct stat sb2;
+ char *full_name;
+ int flags = O_WRONLY | O_CREAT | O_EXCL;
+ int new_fd;
+ printf ("%s %s\n", ent_name, parent_dir);
+ full_name = file_name_concat (parent_dir, ent_name, NULL);
+ new_fd =
+ (fd == AT_FDCWD
+ ? open (full_name, flags, 0600)
+ : openat (fd, ent_name, flags, 0600));
+ if (new_fd < 0)
+ error (1, errno, _("failed to create %s in %s"), ent_name, parent_dir);
+ assert (fstat (new_fd, &sb1) == 0);
+ close (new_fd);
+ free (parent_dir);
+ e = lstat (full_name, &sb2);
+ if (e)
+ error (0, errno, _("lstat failed for %s"), full_name);
+ if ( ! (e == 0 || errno == ELOOP))
+ error (1, errno, _("unexpected errno after lstat of %s"), full_name);
+ assert (e != 0 || SAME_INODE (sb1, sb2));
+ if (fd != AT_FDCWD)
+ close (fd);
+ free (ent_name);
+ free (full_name);
+ }
+}
+
+static void
+t1 (void)
+{
+ /* Given the symlink chain foo/bar -> baz/bog -> $PWD/no-file, this snippet,
+ int fd = AT_FDCWD; char *val = traverse_symlink (&fd, "foo", "bar", ".");
+ would leave fd open on "foo", and return "baz/bog". The next call,
+ char *val = traverse_symlink (&fd, "baz", "bog", "foo");
+ would leave fd open on "baz", and return "$PWD/no-file". The final call
+ char *val = traverse_symlink (&fd, "$PWD", "no-file", ".");
+ would return NULL with say, errno = ENOENT. */
+ assert (mkdir ("foo", 0700) == 0);
+ assert (mkdir ("foo/baz", 0700) == 0);
+ assert (unlink ("no-file") == 0 || errno == ENOENT);
+ assert (symlink ("baz/bog", "foo/bar") == 0);
+ char *pwd = xgetcwd ();
+ char *no_file = file_name_concat (pwd, "no-file", NULL);
+ assert (symlink (no_file, "foo/baz/bog") == 0);
+
+ /* The following works, but would require exposing the internal
+ traverse_symlink function. Not worth it. */
+#if 0
+ char *traverse_symlink (int *fd, char const *dir, char const *ent,
+ char const *cwd_full_name);
+ {
+ int fd = AT_FDCWD;
+ char *val = traverse_symlink (&fd, "foo", "bar", ".");
+ assert (val && STREQ (val, "baz/bog"));
+ free (val);
+
+ val = traverse_symlink (&fd, "baz", "bog", "foo");
+ assert (val && STREQ (val, no_file));
+ free (val);
+
+ val = traverse_symlink (&fd, pwd, "no-file", ".");
+ assert (val == NULL && errno == ENOENT);
+ free (val);
+ assert (close (fd) == 0);
+ }
+#endif
+
+ check_one ("foo/bar");
+ system ("rm -rf foo");
+}
+
+int
+main (int argc, char **argv)
+{
+ if (argc < 2)
+ t1 ();
+ else
+ check_one (argv[1]);
+ return 0;
+}
+
+/*
+Local Variables:
+indent-tabs-mode: nil
+End:
+*/
diff --git a/gl/tests/test-resolve-symlink.sh b/gl/tests/test-resolve-symlink.sh
new file mode 100755
index 0000000..39548ac
--- /dev/null
+++ b/gl/tests/test-resolve-symlink.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+test "$VERBOSE" = yes && set -x
+
+pwd=`pwd`
+t0=`echo "$0"|sed 's,.*/,,'`.tmp; tmp=$t0/$$
+trap 'status=$?; cd "$pwd" && chmod -R u+rwx $t0 && rm -rf $t0 && exit
$status' 0
+trap '(exit $?); exit $?' 1 2 13 15
+
+PATH=$pwd:$PATH
+export PATH
+
+framework_failure=0
+mkdir -p $tmp || framework_failure=1
+cd $tmp || framework_failure=1
+
+( for i in `seq 10`; do ln -s b/a a && mkdir b && cd b; done; )
+( for i in `seq 10`; do ln -s d/c c && mkdir d && cd d; done;
+ ln -s no-such c )
+
+if test $framework_failure = 1; then
+ echo "$0: failure in testing framework" 1>&2
+ (exit 1); exit 1
+fi
+
+fail=0
+
+test-resolve-symlink$EXEEXT a > out || fail=1
+cat <<\EOF > exp || fail=1
+a b/b/b/b/b/b/b/b/b/b
+EOF
+diff out exp || fail=1
+
+test-resolve-symlink$EXEEXT c > out || fail=1
+cat <<EOF > exp || fail=1
+no-such d/d/d/d/d/d/d/d/d/d
+EOF
+diff out exp || fail=1
+
+# Run again, now that "no-such" has been created.
+test-resolve-symlink$EXEEXT c > out || fail=1
+diff out exp || fail=1
+
+# Use an absolute name, relative symlink val.
+ln -s nowhere d2
+test-resolve-symlink$EXEEXT `pwd`/d2 > out || fail=1
+
+# Use a relative name, absolute symlink val.
+ln -s `pwd`/nulle-part d3
+test-resolve-symlink$EXEEXT d3 > out || fail=1
+
+# Invoke with no arguments to run its internal test.
+test-resolve-symlink$EXEEXT > out || fail=1
+p=`pwd -P`
+echo "no-file $p" > exp || fail=1
+diff out exp || fail=1
+
+# Exercise the last-resort code (after failed openat)
+# via a symlink through an unreadable directory.
+mkdir unreadable
+chmod u=wx,go= unreadable
+ln -s unreadable/x d4
+test-resolve-symlink$EXEEXT d4 > out || fail=1
+echo "x unreadable" > exp || fail=1
+diff out exp || fail=1
+
+(exit $fail); exit $fail
diff --git a/src/copy.c b/src/copy.c
index 92588bf..138fc01 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -33,7 +33,6 @@
#include "acl.h"
#include "backupfile.h"
#include "buffer-lcm.h"
-#include "canonicalize.h"
#include "copy.h"
#include "cp-hash.h"
#include "euidaccess.h"
@@ -46,6 +45,7 @@
#include "hash.h"
#include "hash-triple.h"
#include "lchmod.h"
+#include "resolve-symlink.h"
#include "quote.h"
#include "same.h"
#include "savedir.h"
@@ -377,16 +377,29 @@ copy_reg (char const *src_name, char const *dst_name,
if (lstat (dst_name, &dangling_link_sb) == 0
&& S_ISLNK (dangling_link_sb.st_mode))
{
- /* FIXME: This is way overkill, since all that's needed
- is to follow the symlink that is the last file name
- component. */
- name_alloc =
- canonicalize_filename_mode (dst_name, CAN_MISSING);
- if (name_alloc)
+ int fd;
+ char *ent_name;
+ char *parent_dir;
+ int err = resolve_symlink (dst_name, &fd, &ent_name, &parent_dir);
+ char *full_name = NULL;
+ if (err == 0
+ && (full_name = mfile_name_concat (parent_dir,
+ ent_name, NULL)))
+ {
+ dest_desc = (fd == AT_FDCWD
+ ? open (full_name, open_flags,
+ dst_mode & ~omitted_permissions)
+ : openat (fd, ent_name, open_flags,
+ dst_mode & ~omitted_permissions));
+ if (dest_desc < 0)
+ dest_errno = errno;
+ free (ent_name);
+
+ /* Free this memory carefully, below. */
+ followed_dest_name = full_name;
+ }
+ else
{
- followed_dest_name = name_alloc;
- dest_desc = open (followed_dest_name, open_flags,
- dst_mode & ~omitted_permissions);
dest_errno = errno;
}
}
@@ -654,6 +667,8 @@ close_src_desc:
return_val = false;
}
+ if (followed_dest_name != dst_name)
+ free ((char *) followed_dest_name);
free (buf_alloc);
free (name_alloc);
return return_val;
--
1.5.3-dirty