Merge branch 'jc/apply-beyond-symlink'
authorJunio C Hamano <gitster@pobox.com>
Tue, 3 Mar 2015 22:37:01 +0000 (14:37 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 3 Mar 2015 22:37:01 +0000 (14:37 -0800)
"git apply" was not very careful about reading from, removing,
updating and creating paths outside the working tree (under
--index/--cached) or the current directory (when used as a
replacement for GNU patch).

* jc/apply-beyond-symlink:
  apply: do not touch a file beyond a symbolic link
  apply: do not read from beyond a symbolic link
  apply: do not read from the filesystem under --index
  apply: reject input that touches outside the working area

1  2 
builtin/apply.c
t/t4122-apply-symlink-inside.sh

diff --combined builtin/apply.c
@@@ -7,7 -7,6 +7,7 @@@
   *
   */
  #include "cache.h"
 +#include "lockfile.h"
  #include "cache-tree.h"
  #include "quote.h"
  #include "blob.h"
@@@ -51,11 -50,12 +51,12 @@@ static int apply_verbosely
  static int allow_overlap;
  static int no_add;
  static int threeway;
+ static int unsafe_paths;
  static const char *fake_ancestor;
  static int line_termination = '\n';
  static unsigned int p_context = UINT_MAX;
  static const char * const apply_usage[] = {
 -      N_("git apply [options] [<patch>...]"),
 +      N_("git apply [<options>] [<patch>...]"),
        NULL
  };
  
@@@ -301,13 -301,11 +302,13 @@@ static int fuzzy_matchlines(const char 
        while ((*last2 == '\r') || (*last2 == '\n'))
                last2--;
  
 -      /* skip leading whitespace */
 -      while (isspace(*s1) && (s1 <= last1))
 -              s1++;
 -      while (isspace(*s2) && (s2 <= last2))
 -              s2++;
 +      /* skip leading whitespaces, if both begin with whitespace */
 +      if (s1 <= last1 && s2 <= last2 && isspace(*s1) && isspace(*s2)) {
 +              while (isspace(*s1) && (s1 <= last1))
 +                      s1++;
 +              while (isspace(*s2) && (s2 <= last2))
 +                      s2++;
 +      }
        /* early return if both lines are empty */
        if ((s1 > last1) && (s2 > last2))
                return 1;
@@@ -436,7 -434,7 +437,7 @@@ static unsigned long linelen(const cha
  
  static int is_dev_null(const char *str)
  {
 -      return !memcmp("/dev/null", str, 9) && isspace(str[9]);
 +      return skip_prefix(str, "/dev/null", &str) && isspace(*str);
  }
  
  #define TERM_SPACE    1
@@@ -657,6 -655,11 +658,6 @@@ static size_t diff_timestamp_len(const 
        return line + len - end;
  }
  
 -static char *null_strdup(const char *s)
 -{
 -      return s ? xstrdup(s) : NULL;
 -}
 -
  static char *find_name_common(const char *line, const char *def,
                              int p_value, const char *end, int terminate)
  {
                        start = line;
        }
        if (!start)
 -              return squash_slash(null_strdup(def));
 +              return squash_slash(xstrdup_or_null(def));
        len = line - start;
        if (!len)
 -              return squash_slash(null_strdup(def));
 +              return squash_slash(xstrdup_or_null(def));
  
        /*
         * Generally we prefer the shorter name, especially
@@@ -904,7 -907,7 +905,7 @@@ static void parse_traditional_patch(con
                        patch->old_name = name;
                } else {
                        patch->old_name = name;
 -                      patch->new_name = null_strdup(name);
 +                      patch->new_name = xstrdup_or_null(name);
                }
        }
        if (!name)
@@@ -993,7 -996,7 +994,7 @@@ static int gitdiff_delete(const char *l
  {
        patch->is_delete = 1;
        free(patch->old_name);
 -      patch->old_name = null_strdup(patch->def_name);
 +      patch->old_name = xstrdup_or_null(patch->def_name);
        return gitdiff_oldmode(line, patch);
  }
  
@@@ -1001,7 -1004,7 +1002,7 @@@ static int gitdiff_newfile(const char *
  {
        patch->is_new = 1;
        free(patch->new_name);
 -      patch->new_name = null_strdup(patch->def_name);
 +      patch->new_name = xstrdup_or_null(patch->def_name);
        return gitdiff_newmode(line, patch);
  }
  
@@@ -1071,7 -1074,7 +1072,7 @@@ static int gitdiff_index(const char *li
  
        line = ptr + 2;
        ptr = strchr(line, ' ');
 -      eol = strchr(line, '\n');
 +      eol = strchrnul(line, '\n');
  
        if (!ptr || eol < ptr)
                ptr = eol;
@@@ -1277,7 -1280,9 +1278,7 @@@ static int parse_git_header(const char 
         */
        patch->def_name = git_header_name(line, len);
        if (patch->def_name && root) {
 -              char *s = xmalloc(root_len + strlen(patch->def_name) + 1);
 -              strcpy(s, root);
 -              strcpy(s + root_len, patch->def_name);
 +              char *s = xstrfmt("%s%s", root, patch->def_name);
                free(patch->def_name);
                patch->def_name = s;
        }
@@@ -1405,10 -1410,10 +1406,10 @@@ static void recount_diff(const char *li
                case '\\':
                        continue;
                case '@':
 -                      ret = size < 3 || prefixcmp(line, "@@ ");
 +                      ret = size < 3 || !starts_with(line, "@@ ");
                        break;
                case 'd':
 -                      ret = size < 5 || prefixcmp(line, "diff ");
 +                      ret = size < 5 || !starts_with(line, "diff ");
                        break;
                default:
                        ret = -1;
@@@ -1794,11 -1799,11 +1795,11 @@@ static struct fragment *parse_binary_hu
  
        *status_p = 0;
  
 -      if (!prefixcmp(buffer, "delta ")) {
 +      if (starts_with(buffer, "delta ")) {
                patch_method = BINARY_DELTA_DEFLATED;
                origlen = strtoul(buffer + 6, NULL, 10);
        }
 -      else if (!prefixcmp(buffer, "literal ")) {
 +      else if (starts_with(buffer, "literal ")) {
                patch_method = BINARY_LITERAL_DEFLATED;
                origlen = strtoul(buffer + 8, NULL, 10);
        }
@@@ -1916,66 -1921,6 +1917,66 @@@ static int parse_binary(char *buffer, u
        return used;
  }
  
 +static void prefix_one(char **name)
 +{
 +      char *old_name = *name;
 +      if (!old_name)
 +              return;
 +      *name = xstrdup(prefix_filename(prefix, prefix_length, *name));
 +      free(old_name);
 +}
 +
 +static void prefix_patch(struct patch *p)
 +{
 +      if (!prefix || p->is_toplevel_relative)
 +              return;
 +      prefix_one(&p->new_name);
 +      prefix_one(&p->old_name);
 +}
 +
 +/*
 + * include/exclude
 + */
 +
 +static struct string_list limit_by_name;
 +static int has_include;
 +static void add_name_limit(const char *name, int exclude)
 +{
 +      struct string_list_item *it;
 +
 +      it = string_list_append(&limit_by_name, name);
 +      it->util = exclude ? NULL : (void *) 1;
 +}
 +
 +static int use_patch(struct patch *p)
 +{
 +      const char *pathname = p->new_name ? p->new_name : p->old_name;
 +      int i;
 +
 +      /* Paths outside are not touched regardless of "--include" */
 +      if (0 < prefix_length) {
 +              int pathlen = strlen(pathname);
 +              if (pathlen <= prefix_length ||
 +                  memcmp(prefix, pathname, prefix_length))
 +                      return 0;
 +      }
 +
 +      /* See if it matches any of exclude/include rule */
 +      for (i = 0; i < limit_by_name.nr; i++) {
 +              struct string_list_item *it = &limit_by_name.items[i];
 +              if (!wildmatch(it->string, pathname, 0, NULL))
 +                      return (it->util != NULL);
 +      }
 +
 +      /*
 +       * If we had any include, a path that does not match any rule is
 +       * not used.  Otherwise, we saw bunch of exclude rules (or none)
 +       * and such a path is used.
 +       */
 +      return !has_include;
 +}
 +
 +
  /*
   * Read the patch text in "buffer" that extends for "size" bytes; stop
   * reading after seeing a single patch (i.e. changes to a single file).
@@@ -1991,20 -1936,21 +1992,20 @@@ static int parse_chunk(char *buffer, un
        if (offset < 0)
                return offset;
  
 -      patch->ws_rule = whitespace_rule(patch->new_name
 -                                       ? patch->new_name
 -                                       : patch->old_name);
 +      prefix_patch(patch);
 +
 +      if (!use_patch(patch))
 +              patch->ws_rule = 0;
 +      else
 +              patch->ws_rule = whitespace_rule(patch->new_name
 +                                               ? patch->new_name
 +                                               : patch->old_name);
  
        patchsize = parse_single_patch(buffer + offset + hdrsize,
                                       size - offset - hdrsize, patch);
  
        if (!patchsize) {
 -              static const char *binhdr[] = {
 -                      "Binary files ",
 -                      "Files ",
 -                      NULL,
 -              };
                static const char git_binary[] = "GIT binary patch\n";
 -              int i;
                int hd = hdrsize + offset;
                unsigned long llen = linelen(buffer + hd, size - hd);
  
                                patchsize = 0;
                }
                else if (!memcmp(" differ\n", buffer + hd + llen - 8, 8)) {
 +                      static const char *binhdr[] = {
 +                              "Binary files ",
 +                              "Files ",
 +                              NULL,
 +                      };
 +                      int i;
                        for (i = 0; binhdr[i]; i++) {
                                int len = strlen(binhdr[i]);
                                if (len < size - hd &&
@@@ -2230,12 -2170,6 +2231,12 @@@ static void update_pre_post_images(stru
                ctx++;
        }
  
 +      if (postlen
 +          ? postlen < new - postimage->buf
 +          : postimage->len < new - postimage->buf)
 +              die("BUG: caller miscounted postlen: asked %d, orig = %d, used = %d",
 +                  (int)postlen, (int) postimage->len, (int)(new - postimage->buf));
 +
        /* Fix the length of the whole thing */
        postimage->len = new - postimage->buf;
        postimage->nr -= reduced;
@@@ -2391,27 -2325,10 +2392,27 @@@ static int match_fragment(struct image 
  
        /*
         * The hunk does not apply byte-by-byte, but the hash says
 -       * it might with whitespace fuzz. We haven't been asked to
 +       * it might with whitespace fuzz. We weren't asked to
         * ignore whitespace, we were asked to correct whitespace
         * errors, so let's try matching after whitespace correction.
         *
 +       * While checking the preimage against the target, whitespace
 +       * errors in both fixed, we count how large the corresponding
 +       * postimage needs to be.  The postimage prepared by
 +       * apply_one_fragment() has whitespace errors fixed on added
 +       * lines already, but the common lines were propagated as-is,
 +       * which may become longer when their whitespace errors are
 +       * fixed.
 +       */
 +
 +      /* First count added lines in postimage */
 +      postlen = 0;
 +      for (i = 0; i < postimage->nr; i++) {
 +              if (!(postimage->line[i].flag & LINE_COMMON))
 +                      postlen += postimage->line[i].len;
 +      }
 +
 +      /*
         * The preimage may extend beyond the end of the file,
         * but in this loop we will only handle the part of the
         * preimage that falls within the file.
        strbuf_init(&fixed, preimage->len + 1);
        orig = preimage->buf;
        target = img->buf + try;
 -      postlen = 0;
        for (i = 0; i < preimage_limit; i++) {
                size_t oldlen = preimage->line[i].len;
                size_t tgtlen = img->line[try_lno + i].len;
                match = (tgtfix.len == fixed.len - fixstart &&
                         !memcmp(tgtfix.buf, fixed.buf + fixstart,
                                             fixed.len - fixstart));
 -              postlen += tgtfix.len;
 +
 +              /* Add the length if this is common with the postimage */
 +              if (preimage->line[i].flag & LINE_COMMON)
 +                      postlen += tgtfix.len;
  
                strbuf_release(&tgtfix);
                if (!match)
@@@ -2647,7 -2562,7 +2648,7 @@@ static void update_image(struct image *
                 * NOTE: this knows that we never call remove_first_line()
                 * on anything other than pre/post image.
                 */
 -              img->line = xrealloc(img->line, nr * sizeof(*img->line));
 +              REALLOC_ARRAY(img->line, nr);
                img->line_allocated = img->line;
        }
        if (preimage_limit != postimage->nr)
@@@ -2953,7 -2868,9 +2954,7 @@@ static int apply_binary_fragment(struc
        case BINARY_LITERAL_DEFLATED:
                clear_image(img);
                img->len = fragment->size;
 -              img->buf = xmalloc(img->len+1);
 -              memcpy(img->buf, fragment->patch, img->len);
 -              img->buf[img->len] = '\0';
 +              img->buf = xmemdupz(fragment->patch, img->len);
                return 0;
        }
        return -1;
@@@ -3168,15 -3085,13 +3169,15 @@@ static void prepare_fn_table(struct pat
        }
  }
  
 -static int checkout_target(struct cache_entry *ce, struct stat *st)
 +static int checkout_target(struct index_state *istate,
 +                         struct cache_entry *ce, struct stat *st)
  {
        struct checkout costate;
  
        memset(&costate, 0, sizeof(costate));
        costate.base_dir = "";
        costate.refresh_cache = 1;
 +      costate.istate = istate;
        if (checkout_entry(ce, &costate, NULL) || lstat(ce->name, st))
                return error(_("cannot checkout %s"), ce->name);
        return 0;
@@@ -3221,7 -3136,7 +3222,7 @@@ static int load_patch_target(struct str
                             const char *name,
                             unsigned expected_mode)
  {
-       if (cached) {
+       if (cached || check_index) {
                if (read_file_or_gitlink(ce, buf))
                        return error(_("read of %s failed"), name);
        } else if (name) {
                                return read_file_or_gitlink(ce, buf);
                        else
                                return SUBMODULE_PATCH_WITHOUT_INDEX;
+               } else if (has_symlink_leading_path(name, strlen(name))) {
+                       return error(_("reading from '%s' beyond a symbolic link"), name);
                } else {
                        if (read_old_data(st, name, buf))
                                return error(_("read of %s failed"), name);
@@@ -3343,7 -3260,7 +3346,7 @@@ static int load_current(struct image *i
        if (lstat(name, &st)) {
                if (errno != ENOENT)
                        return error(_("%s: %s"), name, strerror(errno));
 -              if (checkout_target(ce, &st))
 +              if (checkout_target(&the_index, ce, &st))
                        return -1;
        }
        if (verify_index_match(ce, &st))
@@@ -3497,7 -3414,7 +3500,7 @@@ static int check_preimage(struct patch 
                }
                *ce = active_cache[pos];
                if (stat_ret < 0) {
 -                      if (checkout_target(*ce, st))
 +                      if (checkout_target(&the_index, *ce, st))
                                return -1;
                }
                if (!cached && verify_index_match(*ce, st))
@@@ -3569,6 -3486,121 +3572,121 @@@ static int check_to_create(const char *
        return 0;
  }
  
+ /*
+  * We need to keep track of how symlinks in the preimage are
+  * manipulated by the patches.  A patch to add a/b/c where a/b
+  * is a symlink should not be allowed to affect the directory
+  * the symlink points at, but if the same patch removes a/b,
+  * it is perfectly fine, as the patch removes a/b to make room
+  * to create a directory a/b so that a/b/c can be created.
+  */
+ static struct string_list symlink_changes;
+ #define SYMLINK_GOES_AWAY 01
+ #define SYMLINK_IN_RESULT 02
+ static uintptr_t register_symlink_changes(const char *path, uintptr_t what)
+ {
+       struct string_list_item *ent;
+       ent = string_list_lookup(&symlink_changes, path);
+       if (!ent) {
+               ent = string_list_insert(&symlink_changes, path);
+               ent->util = (void *)0;
+       }
+       ent->util = (void *)(what | ((uintptr_t)ent->util));
+       return (uintptr_t)ent->util;
+ }
+ static uintptr_t check_symlink_changes(const char *path)
+ {
+       struct string_list_item *ent;
+       ent = string_list_lookup(&symlink_changes, path);
+       if (!ent)
+               return 0;
+       return (uintptr_t)ent->util;
+ }
+ static void prepare_symlink_changes(struct patch *patch)
+ {
+       for ( ; patch; patch = patch->next) {
+               if ((patch->old_name && S_ISLNK(patch->old_mode)) &&
+                   (patch->is_rename || patch->is_delete))
+                       /* the symlink at patch->old_name is removed */
+                       register_symlink_changes(patch->old_name, SYMLINK_GOES_AWAY);
+               if (patch->new_name && S_ISLNK(patch->new_mode))
+                       /* the symlink at patch->new_name is created or remains */
+                       register_symlink_changes(patch->new_name, SYMLINK_IN_RESULT);
+       }
+ }
+ static int path_is_beyond_symlink_1(struct strbuf *name)
+ {
+       do {
+               unsigned int change;
+               while (--name->len && name->buf[name->len] != '/')
+                       ; /* scan backwards */
+               if (!name->len)
+                       break;
+               name->buf[name->len] = '\0';
+               change = check_symlink_changes(name->buf);
+               if (change & SYMLINK_IN_RESULT)
+                       return 1;
+               if (change & SYMLINK_GOES_AWAY)
+                       /*
+                        * This cannot be "return 0", because we may
+                        * see a new one created at a higher level.
+                        */
+                       continue;
+               /* otherwise, check the preimage */
+               if (check_index) {
+                       struct cache_entry *ce;
+                       ce = cache_file_exists(name->buf, name->len, ignore_case);
+                       if (ce && S_ISLNK(ce->ce_mode))
+                               return 1;
+               } else {
+                       struct stat st;
+                       if (!lstat(name->buf, &st) && S_ISLNK(st.st_mode))
+                               return 1;
+               }
+       } while (1);
+       return 0;
+ }
+ static int path_is_beyond_symlink(const char *name_)
+ {
+       int ret;
+       struct strbuf name = STRBUF_INIT;
+       assert(*name_ != '\0');
+       strbuf_addstr(&name, name_);
+       ret = path_is_beyond_symlink_1(&name);
+       strbuf_release(&name);
+       return ret;
+ }
+ static void die_on_unsafe_path(struct patch *patch)
+ {
+       const char *old_name = NULL;
+       const char *new_name = NULL;
+       if (patch->is_delete)
+               old_name = patch->old_name;
+       else if (!patch->is_new && !patch->is_copy)
+               old_name = patch->old_name;
+       if (!patch->is_delete)
+               new_name = patch->new_name;
+       if (old_name && !verify_path(old_name))
+               die(_("invalid path '%s'"), old_name);
+       if (new_name && !verify_path(new_name))
+               die(_("invalid path '%s'"), new_name);
+ }
  /*
   * Check and apply the patch in-core; leave the result in patch->result
   * for the caller to write it out to the final destination.
@@@ -3656,6 -3688,22 +3774,22 @@@ static int check_patch(struct patch *pa
                }
        }
  
+       if (!unsafe_paths)
+               die_on_unsafe_path(patch);
+       /*
+        * An attempt to read from or delete a path that is beyond a
+        * symbolic link will be prevented by load_patch_target() that
+        * is called at the beginning of apply_data() so we do not
+        * have to worry about a patch marked with "is_delete" bit
+        * here.  We however need to make sure that the patch result
+        * is not deposited to a path that is beyond a symbolic link
+        * here.
+        */
+       if (!patch->is_delete && path_is_beyond_symlink(patch->new_name))
+               return error(_("affected file '%s' is beyond a symbolic link"),
+                            patch->new_name);
        if (apply_data(patch, &st, ce) < 0)
                return error(_("%s: patch does not apply"), name);
        patch->rejected = 0;
@@@ -3666,6 -3714,7 +3800,7 @@@ static int check_patch_list(struct patc
  {
        int err = 0;
  
+       prepare_symlink_changes(patch);
        prepare_fn_table(patch);
        while (patch) {
                if (apply_verbosely)
@@@ -3713,12 -3762,12 +3848,12 @@@ static int preimage_sha1_in_gitlink_pat
            hunk->oldpos == 1 && hunk->oldlines == 1 &&
            /* does preimage begin with the heading? */
            (preimage = memchr(hunk->patch, '\n', hunk->size)) != NULL &&
 -          !prefixcmp(++preimage, heading) &&
 +          starts_with(++preimage, heading) &&
            /* does it record full SHA-1? */
            !get_sha1_hex(preimage + sizeof(heading) - 1, sha1) &&
            preimage[sizeof(heading) + 40 - 1] == '\n' &&
            /* does the abbreviated name on the index line agree with it? */
 -          !prefixcmp(preimage + sizeof(heading) - 1, p->old_sha1_prefix))
 +          starts_with(preimage + sizeof(heading) - 1, p->old_sha1_prefix))
                return 0; /* it all looks fine */
  
        /* we may have full object name on the index line */
@@@ -3730,7 -3779,7 +3865,7 @@@ static void build_fake_ancestor(struct 
  {
        struct patch *patch;
        struct index_state result = { NULL };
 -      int fd;
 +      static struct lock_file lock;
  
        /* Once we start supporting the reverse patch, it may be
         * worth showing the new sha1 prefix, but until then...
                        if (!preimage_sha1_in_gitlink_patch(patch, sha1))
                                ; /* ok, the textual part looks sane */
                        else
 -                              die("sha1 information is lacking or useless for submoule %s",
 +                              die("sha1 information is lacking or useless for submodule %s",
                                    name);
                } else if (!get_sha1_blob(patch->old_sha1_prefix, sha1)) {
                        ; /* ok */
                        die ("Could not add %s to temporary index", name);
        }
  
 -      fd = open(filename, O_WRONLY | O_CREAT, 0666);
 -      if (fd < 0 || write_index(&result, fd) || close(fd))
 +      hold_lock_file_for_update(&lock, filename, LOCK_DIE_ON_ERROR);
 +      if (write_locked_index(&result, &lock, COMMIT_LOCK))
                die ("Could not write temporary index to %s", filename);
  
        discard_index(&result);
@@@ -3931,10 -3980,9 +4066,10 @@@ static void add_index_file(const char *
        ce->ce_flags = create_ce_flags(0);
        ce->ce_namelen = namelen;
        if (S_ISGITLINK(mode)) {
 -              const char *s = buf;
 +              const char *s;
  
 -              if (get_sha1_hex(s + strlen("Subproject commit "), ce->sha1))
 +              if (!skip_prefix(buf, "Subproject commit ", &s) ||
 +                  get_sha1_hex(s, ce->sha1))
                        die(_("corrupt patch for submodule %s"), path);
        } else {
                if (!cached) {
@@@ -4148,7 -4196,7 +4283,7 @@@ static int write_out_one_reject(struct 
                return error(_("cannot open %s: %s"), namebuf, strerror(errno));
  
        /* Normal git tools never deal with .rej, so do not pretend
 -       * this is a git patch by saying --git nor give extended
 +       * this is a git patch by saying --git or giving extended
         * headers.  While at it, maybe please "kompare" that wants
         * the trailing TAB and some garbage at the end of line ;-).
         */
@@@ -4200,7 -4248,7 +4335,7 @@@ static int write_out_results(struct pat
        if (cpath.nr) {
                struct string_list_item *item;
  
 -              sort_string_list(&cpath);
 +              string_list_sort(&cpath);
                for_each_string_list_item(item, &cpath)
                        fprintf(stderr, "U %s\n", item->string);
                string_list_clear(&cpath, 0);
  
  static struct lock_file lock_file;
  
 -static struct string_list limit_by_name;
 -static int has_include;
 -static void add_name_limit(const char *name, int exclude)
 -{
 -      struct string_list_item *it;
 -
 -      it = string_list_append(&limit_by_name, name);
 -      it->util = exclude ? NULL : (void *) 1;
 -}
 -
 -static int use_patch(struct patch *p)
 -{
 -      const char *pathname = p->new_name ? p->new_name : p->old_name;
 -      int i;
 -
 -      /* Paths outside are not touched regardless of "--include" */
 -      if (0 < prefix_length) {
 -              int pathlen = strlen(pathname);
 -              if (pathlen <= prefix_length ||
 -                  memcmp(prefix, pathname, prefix_length))
 -                      return 0;
 -      }
 -
 -      /* See if it matches any of exclude/include rule */
 -      for (i = 0; i < limit_by_name.nr; i++) {
 -              struct string_list_item *it = &limit_by_name.items[i];
 -              if (!fnmatch(it->string, pathname, 0))
 -                      return (it->util != NULL);
 -      }
 -
 -      /*
 -       * If we had any include, a path that does not match any rule is
 -       * not used.  Otherwise, we saw bunch of exclude rules (or none)
 -       * and such a path is used.
 -       */
 -      return !has_include;
 -}
 -
 -
 -static void prefix_one(char **name)
 -{
 -      char *old_name = *name;
 -      if (!old_name)
 -              return;
 -      *name = xstrdup(prefix_filename(prefix, prefix_length, *name));
 -      free(old_name);
 -}
 -
 -static void prefix_patches(struct patch *p)
 -{
 -      if (!prefix || p->is_toplevel_relative)
 -              return;
 -      for ( ; p; p = p->next) {
 -              prefix_one(&p->new_name);
 -              prefix_one(&p->old_name);
 -      }
 -}
 -
  #define INACCURATE_EOF        (1<<0)
  #define RECOUNT               (1<<1)
  
@@@ -4238,6 -4344,8 +4373,6 @@@ static int apply_patch(int fd, const ch
                        break;
                if (apply_in_reverse)
                        reverse_patches(patch);
 -              if (prefix)
 -                      prefix_patches(patch);
                if (use_patch(patch)) {
                        patch_stats(patch);
                        *listp = patch;
        return 0;
  }
  
 -static int git_apply_config(const char *var, const char *value, void *cb)
 +static void git_apply_config(void)
  {
 -      if (!strcmp(var, "apply.whitespace"))
 -              return git_config_string(&apply_default_whitespace, var, value);
 -      else if (!strcmp(var, "apply.ignorewhitespace"))
 -              return git_config_string(&apply_default_ignorewhitespace, var, value);
 -      return git_default_config(var, value, cb);
 +      git_config_get_string_const("apply.whitespace", &apply_default_whitespace);
 +      git_config_get_string_const("apply.ignorewhitespace", &apply_default_ignorewhitespace);
 +      git_config(git_default_config, NULL);
  }
  
  static int option_parse_exclude(const struct option *opt,
@@@ -4404,6 -4514,8 +4539,8 @@@ int cmd_apply(int argc, const char **ar
                        N_("make sure the patch is applicable to the current index")),
                OPT_BOOL(0, "cached", &cached,
                        N_("apply a patch without touching the working tree")),
+               OPT_BOOL(0, "unsafe-paths", &unsafe_paths,
+                       N_("accept a patch that touches outside the working area")),
                OPT_BOOL(0, "apply", &force_apply,
                        N_("also apply the patch (use with --stat/--summary/--check)")),
                OPT_BOOL('3', "3way", &threeway,
  
        prefix = prefix_;
        prefix_length = prefix ? strlen(prefix) : 0;
 -      git_config(git_apply_config, NULL);
 +      git_apply_config();
        if (apply_default_whitespace)
                parse_whitespace_option(apply_default_whitespace);
        if (apply_default_ignorewhitespace)
                        die(_("--cached outside a repository"));
                check_index = 1;
        }
+       if (check_index)
+               unsafe_paths = 0;
        for (i = 0; i < argc; i++) {
                const char *arg = argv[i];
                int fd;
        }
  
        if (update_index) {
 -              if (write_cache(newfd, active_cache, active_nr) ||
 -                  commit_locked_index(&lock_file))
 +              if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
                        die(_("Unable to write new index file"));
        }
  
@@@ -3,10 -3,17 +3,10 @@@
  test_description='apply to deeper directory without getting fooled with symlink'
  . ./test-lib.sh
  
 -lecho () {
 -      for l_
 -      do
 -              echo "$l_"
 -      done
 -}
 -
  test_expect_success setup '
  
        mkdir -p arch/i386/boot arch/x86_64 &&
 -      lecho 1 2 3 4 5 >arch/i386/boot/Makefile &&
 +      test_write_lines 1 2 3 4 5 >arch/i386/boot/Makefile &&
        test_ln_s_add ../i386/boot arch/x86_64/boot &&
        git add . &&
        test_tick &&
@@@ -15,7 -22,7 +15,7 @@@
  
        rm arch/x86_64/boot &&
        mkdir arch/x86_64/boot &&
 -      lecho 2 3 4 5 6 >arch/x86_64/boot/Makefile &&
 +      test_write_lines 2 3 4 5 6 >arch/x86_64/boot/Makefile &&
        git add . &&
        test_tick &&
        git commit -a -m second &&
@@@ -45,4 -52,110 +45,110 @@@ test_expect_success 'check result' 
  
  '
  
+ test_expect_success SYMLINKS 'do not read from beyond symbolic link' '
+       git reset --hard &&
+       mkdir -p arch/x86_64/dir &&
+       >arch/x86_64/dir/file &&
+       git add arch/x86_64/dir/file &&
+       echo line >arch/x86_64/dir/file &&
+       git diff >patch &&
+       git reset --hard &&
+       mkdir arch/i386/dir &&
+       >arch/i386/dir/file &&
+       ln -s ../i386/dir arch/x86_64/dir &&
+       test_must_fail git apply patch &&
+       test_must_fail git apply --cached patch &&
+       test_must_fail git apply --index patch
+ '
+ test_expect_success SYMLINKS 'do not follow symbolic link (setup)' '
+       rm -rf arch/i386/dir arch/x86_64/dir &&
+       git reset --hard &&
+       ln -s ../i386/dir arch/x86_64/dir &&
+       git add arch/x86_64/dir &&
+       git diff HEAD >add_symlink.patch &&
+       git reset --hard &&
+       mkdir arch/x86_64/dir &&
+       >arch/x86_64/dir/file &&
+       git add arch/x86_64/dir/file &&
+       git diff HEAD >add_file.patch &&
+       git diff -R HEAD >del_file.patch &&
+       git reset --hard &&
+       rm -fr arch/x86_64/dir &&
+       cat add_symlink.patch add_file.patch >patch &&
+       cat add_symlink.patch del_file.patch >tricky_del &&
+       mkdir arch/i386/dir
+ '
+ test_expect_success SYMLINKS 'do not follow symbolic link (same input)' '
+       # same input creates a confusing symbolic link
+       test_must_fail git apply patch 2>error-wt &&
+       test_i18ngrep "beyond a symbolic link" error-wt &&
+       test_path_is_missing arch/x86_64/dir &&
+       test_path_is_missing arch/i386/dir/file &&
+       test_must_fail git apply --index patch 2>error-ix &&
+       test_i18ngrep "beyond a symbolic link" error-ix &&
+       test_path_is_missing arch/x86_64/dir &&
+       test_path_is_missing arch/i386/dir/file &&
+       test_must_fail git ls-files --error-unmatch arch/x86_64/dir &&
+       test_must_fail git ls-files --error-unmatch arch/i386/dir &&
+       test_must_fail git apply --cached patch 2>error-ct &&
+       test_i18ngrep "beyond a symbolic link" error-ct &&
+       test_must_fail git ls-files --error-unmatch arch/x86_64/dir &&
+       test_must_fail git ls-files --error-unmatch arch/i386/dir &&
+       >arch/i386/dir/file &&
+       git add arch/i386/dir/file &&
+       test_must_fail git apply tricky_del &&
+       test_path_is_file arch/i386/dir/file &&
+       test_must_fail git apply --index tricky_del &&
+       test_path_is_file arch/i386/dir/file &&
+       test_must_fail git ls-files --error-unmatch arch/x86_64/dir &&
+       git ls-files --error-unmatch arch/i386/dir &&
+       test_must_fail git apply --cached tricky_del &&
+       test_must_fail git ls-files --error-unmatch arch/x86_64/dir &&
+       git ls-files --error-unmatch arch/i386/dir
+ '
+ test_expect_success SYMLINKS 'do not follow symbolic link (existing)' '
+       # existing symbolic link
+       git reset --hard &&
+       ln -s ../i386/dir arch/x86_64/dir &&
+       git add arch/x86_64/dir &&
+       test_must_fail git apply add_file.patch 2>error-wt-add &&
+       test_i18ngrep "beyond a symbolic link" error-wt-add &&
+       test_path_is_missing arch/i386/dir/file &&
+       mkdir arch/i386/dir &&
+       >arch/i386/dir/file &&
+       test_must_fail git apply del_file.patch 2>error-wt-del &&
+       test_i18ngrep "beyond a symbolic link" error-wt-del &&
+       test_path_is_file arch/i386/dir/file &&
+       rm arch/i386/dir/file &&
+       test_must_fail git apply --index add_file.patch 2>error-ix-add &&
+       test_i18ngrep "beyond a symbolic link" error-ix-add &&
+       test_path_is_missing arch/i386/dir/file &&
+       test_must_fail git ls-files --error-unmatch arch/i386/dir &&
+       test_must_fail git apply --cached add_file.patch 2>error-ct-file &&
+       test_i18ngrep "beyond a symbolic link" error-ct-file &&
+       test_must_fail git ls-files --error-unmatch arch/i386/dir
+ '
  test_done