Bruno Haible <[EMAIL PROTECTED]> 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 <[EMAIL PROTECTED]> Date: Fri, 3 Aug 2007 18:44:54 +0200 Subject: [PATCH] copy.c: handle dangling destination symlinks safely Signed-off-by: Jim Meyering <[EMAIL PROTECTED]> --- .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 <[EMAIL PROTECTED]> + + 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 <[EMAIL PROTECTED]> 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 <[EMAIL PROTECTED]>. */ + +#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