Skip to content

Commit 9e65df5

Browse files
committed
Merge branch 'ownership-checks-in-local-clones'
This topic addresses two CVEs: - CVE-2024-32020: Local clones may end up hardlinking files into the target repository's object database when source and target repository reside on the same disk. If the source repository is owned by a different user, then those hardlinked files may be rewritten at any point in time by the untrusted user. - CVE-2024-32021: When cloning a local source repository that contains symlinks via the filesystem, Git may create hardlinks to arbitrary user-readable files on the same filesystem as the target repository in the objects/ directory. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 2b3d38a + 1204e1a commit 9e65df5

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

builtin/clone.c

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
320320
int src_len, dest_len;
321321
struct dir_iterator *iter;
322322
int iter_status;
323-
struct strbuf realpath = STRBUF_INIT;
323+
324+
/*
325+
* Refuse copying directories by default which aren't owned by us. The
326+
* code that performs either the copying or hardlinking is not prepared
327+
* to handle various edge cases where an adversary may for example
328+
* racily swap out files for symlinks. This can cause us to
329+
* inadvertently use the wrong source file.
330+
*
331+
* Furthermore, even if we were prepared to handle such races safely,
332+
* creating hardlinks across user boundaries is an inherently unsafe
333+
* operation as the hardlinked files can be rewritten at will by the
334+
* potentially-untrusted user. We thus refuse to do so by default.
335+
*/
336+
die_upon_dubious_ownership(NULL, NULL, src_repo);
324337

325338
mkdir_if_missing(dest->buf, 0777);
326339

@@ -358,9 +371,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
358371
if (unlink(dest->buf) && errno != ENOENT)
359372
die_errno(_("failed to unlink '%s'"), dest->buf);
360373
if (!option_no_hardlinks) {
361-
strbuf_realpath(&realpath, src->buf, 1);
362-
if (!link(realpath.buf, dest->buf))
374+
if (!link(src->buf, dest->buf)) {
375+
struct stat st;
376+
377+
/*
378+
* Sanity-check whether the created hardlink
379+
* actually links to the expected file now. This
380+
* catches time-of-check-time-of-use bugs in
381+
* case the source file was meanwhile swapped.
382+
*/
383+
if (lstat(dest->buf, &st))
384+
die(_("hardlink cannot be checked at '%s'"), dest->buf);
385+
if (st.st_mode != iter->st.st_mode ||
386+
st.st_ino != iter->st.st_ino ||
387+
st.st_dev != iter->st.st_dev ||
388+
st.st_size != iter->st.st_size ||
389+
st.st_uid != iter->st.st_uid ||
390+
st.st_gid != iter->st.st_gid)
391+
die(_("hardlink different from source at '%s'"), dest->buf);
392+
363393
continue;
394+
}
364395
if (option_local > 0)
365396
die_errno(_("failed to create link '%s'"), dest->buf);
366397
option_no_hardlinks = 1;
@@ -373,8 +404,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
373404
strbuf_setlen(src, src_len);
374405
die(_("failed to iterate over '%s'"), src->buf);
375406
}
376-
377-
strbuf_release(&realpath);
378407
}
379408

380409
static void clone_local(const char *src_repo, const char *dest_repo)

t/t0033-safe-directory.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' '
8080
git status
8181
'
8282

83+
test_expect_success 'local clone of unowned repo refused in unsafe directory' '
84+
test_when_finished "rm -rf source" &&
85+
git init source &&
86+
(
87+
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
88+
test_commit -C source initial
89+
) &&
90+
test_must_fail git clone --local source target &&
91+
test_path_is_missing target
92+
'
93+
94+
test_expect_success 'local clone of unowned repo accepted in safe directory' '
95+
test_when_finished "rm -rf source" &&
96+
git init source &&
97+
(
98+
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
99+
test_commit -C source initial
100+
) &&
101+
test_must_fail git clone --local source target &&
102+
git config --global --add safe.directory "$(pwd)/source/.git" &&
103+
git clone --local source target &&
104+
test_path_is_dir target
105+
'
106+
83107
test_done

0 commit comments

Comments
 (0)