Sync with 2.43.4

* maint-2.43: (40 commits)
  Git 2.43.4
  Git 2.42.2
  Git 2.41.1
  Git 2.40.2
  Git 2.39.4
  fsck: warn about symlink pointing inside a gitdir
  core.hooksPath: add some protection while cloning
  init.templateDir: consider this config setting protected
  clone: prevent hooks from running during a clone
  Add a helper function to compare file contents
  init: refactor the template directory discovery into its own function
  find_hook(): refactor the `STRIP_EXTENSION` logic
  clone: when symbolic links collide with directories, keep the latter
  entry: report more colliding paths
  t5510: verify that D/F confusion cannot lead to an RCE
  submodule: require the submodule path to contain directories only
  clone_submodule: avoid using `access()` on directories
  submodules: submodule paths must not contain symlinks
  clone: prevent clashing git dirs when cloning submodule in parallel
  t7423: add tests for symlinked submodule directories
  ...
This commit is contained in:
Johannes Schindelin 2024-04-10 22:10:06 +02:00
commit e5e6663e69
45 changed files with 1281 additions and 87 deletions

View file

@ -0,0 +1,79 @@
Git v2.39.4 Release Notes
=========================
This addresses the security issues CVE-2024-32002, CVE-2024-32004,
CVE-2024-32020 and CVE-2024-32021.
This release also backports fixes necessary to let the CI builds pass
successfully.
Fixes since v2.39.3
-------------------
* CVE-2024-32002:
Recursive clones on case-insensitive filesystems that support symbolic
links are susceptible to case confusion that can be exploited to
execute just-cloned code during the clone operation.
* CVE-2024-32004:
Repositories can be configured to execute arbitrary code during local
clones. To address this, the ownership checks introduced in v2.30.3
are now extended to cover cloning local repositories.
* 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.
* CVE-2024-32465:
It is supposed to be safe to clone untrusted repositories, even those
unpacked from zip archives or tarballs originating from untrusted
sources, but Git can be tricked to run arbitrary code as part of the
clone.
* Defense-in-depth: submodule: require the submodule path to contain
directories only.
* Defense-in-depth: clone: when symbolic links collide with directories, keep
the latter.
* Defense-in-depth: clone: prevent hooks from running during a clone.
* Defense-in-depth: core.hooksPath: add some protection while cloning.
* Defense-in-depth: fsck: warn about symlink pointing inside a gitdir.
* Various fix-ups on HTTP tests.
* Test update.
* HTTP Header redaction code has been adjusted for a newer version of
cURL library that shows its traces differently from earlier
versions.
* Fix was added to work around a regression in libcURL 8.7.0 (which has
already been fixed in their tip of the tree).
* Replace macos-12 used at GitHub CI with macos-13.
* ci(linux-asan/linux-ubsan): let's save some time
* Tests with LSan from time to time seem to emit harmless message that makes
our tests unnecessarily flakey; we work it around by filtering the
uninteresting output.
* Update GitHub Actions jobs to avoid warnings against using deprecated
version of Node.js.

View file

@ -0,0 +1,7 @@
Git v2.40.2 Release Notes
=========================
This release merges up the fix that appears in v2.39.4 to address
the security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020,
CVE-2024-32021 and CVE-2024-32465; see the release notes for that
version for details.

View file

@ -0,0 +1,7 @@
Git v2.41.1 Release Notes
=========================
This release merges up the fix that appears in v2.39.4 and v2.40.2
to address the security issues CVE-2024-32002, CVE-2024-32004,
CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465; see the release
notes for these versions for details.

View file

@ -0,0 +1,7 @@
Git v2.42.2 Release Notes
=========================
This release merges up the fix that appears in v2.39.4, v2.40.2
and v2.41.1 to address the security issues CVE-2024-32002,
CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465;
see the release notes for these versions for details.

View file

@ -0,0 +1,7 @@
Git v2.43.4 Release Notes
=========================
This release merges up the fix that appears in v2.39.4, v2.40.2,
v2.41.1 and v2.42.2 to address the security issues CVE-2024-32002,
CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465;
see the release notes for these versions for details.

View file

@ -164,6 +164,18 @@
`nullSha1`::
(WARN) Tree contains entries pointing to a null sha1.
`symlinkPointsToGitDir`::
(WARN) Symbolic link points inside a gitdir.
`symlinkTargetBlob`::
(ERROR) A non-blob found instead of a symbolic link's target.
`symlinkTargetLength`::
(WARN) Symbolic link target longer than maximum path length.
`symlinkTargetMissing`::
(ERROR) Unable to read symbolic link target's blob.
`treeNotSorted`::
(ERROR) A tree is not properly sorted.

View file

@ -55,6 +55,37 @@ ENVIRONMENT
admins may need to configure some transports to allow this
variable to be passed. See the discussion in linkgit:git[1].
`GIT_NO_LAZY_FETCH`::
When cloning or fetching from a partial repository (i.e., one
itself cloned with `--filter`), the server-side `upload-pack`
may need to fetch extra objects from its upstream in order to
complete the request. By default, `upload-pack` will refuse to
perform such a lazy fetch, because `git fetch` may run arbitrary
commands specified in configuration and hooks of the source
repository (and `upload-pack` tries to be safe to run even in
untrusted `.git` directories).
+
This is implemented by having `upload-pack` internally set the
`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it
(because you are fetching from a partial clone, and you are sure
you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to
`0`.
SECURITY
--------
Most Git commands should not be run in an untrusted `.git` directory
(see the section `SECURITY` in linkgit:git[1]). `upload-pack` tries to
avoid any dangerous configuration options or hooks from the repository
it's serving, making it safe to clone an untrusted directory and run
commands on the resulting clone.
For an extra level of safety, you may be able to run `upload-pack` as an
alternate user. The details will be platform dependent, but on many
systems you can run:
git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' ...
SEE ALSO
--------
linkgit:gitnamespaces[7]

View file

@ -1049,6 +1049,37 @@ The index is also capable of storing multiple entries (called "stages")
for a given pathname. These stages are used to hold the various
unmerged version of a file when a merge is in progress.
SECURITY
--------
Some configuration options and hook files may cause Git to run arbitrary
shell commands. Because configuration and hooks are not copied using
`git clone`, it is generally safe to clone remote repositories with
untrusted content, inspect them with `git log`, and so on.
However, it is not safe to run Git commands in a `.git` directory (or
the working tree that surrounds it) when that `.git` directory itself
comes from an untrusted source. The commands in its config and hooks
are executed in the usual way.
By default, Git will refuse to run when the repository is owned by
someone other than the user running the command. See the entry for
`safe.directory` in linkgit:git-config[1]. While this can help protect
you in a multi-user environment, note that you can also acquire
untrusted repositories that are owned by you (for example, if you
extract a zip file or tarball from an untrusted source). In such cases,
you'd need to "sanitize" the untrusted repository first.
If you have an untrusted `.git` directory, you should first clone it
with `git clone --no-local` to obtain a clean copy. Git does restrict
the set of options and hooks that will be run by `upload-pack`, which
handles the server side of a clone or fetch, but beware that the
surface area for attack against `upload-pack` is large, so this does
carry some risk. The safest thing is to serve the repository as an
unprivileged user (either via linkgit:git-daemon[1], ssh, or using
other tools to change user ids). See the discussion in the `SECURITY`
section of linkgit:git-upload-pack[1].
FURTHER DOCUMENTATION
---------------------

View file

@ -139,7 +139,7 @@ Issues of note:
not need that functionality, use NO_CURL to build without
it.
Git requires version "7.19.5" or later of "libcurl" to build
Git requires version "7.21.3" or later of "libcurl" to build
without NO_CURL. This version requirement may be bumped in
the future.

View file

@ -329,7 +329,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
int src_len, dest_len;
struct dir_iterator *iter;
int iter_status;
struct strbuf realpath = STRBUF_INIT;
/*
* Refuse copying directories by default which aren't owned by us. The
* code that performs either the copying or hardlinking is not prepared
* to handle various edge cases where an adversary may for example
* racily swap out files for symlinks. This can cause us to
* inadvertently use the wrong source file.
*
* Furthermore, even if we were prepared to handle such races safely,
* creating hardlinks across user boundaries is an inherently unsafe
* operation as the hardlinked files can be rewritten at will by the
* potentially-untrusted user. We thus refuse to do so by default.
*/
die_upon_dubious_ownership(NULL, NULL, src_repo);
mkdir_if_missing(dest->buf, 0777);
@ -377,9 +390,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
if (unlink(dest->buf) && errno != ENOENT)
die_errno(_("failed to unlink '%s'"), dest->buf);
if (!option_no_hardlinks) {
strbuf_realpath(&realpath, src->buf, 1);
if (!link(realpath.buf, dest->buf))
if (!link(src->buf, dest->buf)) {
struct stat st;
/*
* Sanity-check whether the created hardlink
* actually links to the expected file now. This
* catches time-of-check-time-of-use bugs in
* case the source file was meanwhile swapped.
*/
if (lstat(dest->buf, &st))
die(_("hardlink cannot be checked at '%s'"), dest->buf);
if (st.st_mode != iter->st.st_mode ||
st.st_ino != iter->st.st_ino ||
st.st_dev != iter->st.st_dev ||
st.st_size != iter->st.st_size ||
st.st_uid != iter->st.st_uid ||
st.st_gid != iter->st.st_gid)
die(_("hardlink different from source at '%s'"), dest->buf);
continue;
}
if (option_local > 0)
die_errno(_("failed to create link '%s'"), dest->buf);
option_no_hardlinks = 1;
@ -392,8 +423,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
strbuf_setlen(src, src_len);
die(_("failed to iterate over '%s'"), src->buf);
}
strbuf_release(&realpath);
}
static void clone_local(const char *src_repo, const char *dest_repo)
@ -936,6 +965,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
int hash_algo;
unsigned int ref_storage_format = REF_STORAGE_FORMAT_UNKNOWN;
const int do_not_override_repo_unix_permissions = -1;
const char *template_dir;
char *template_dir_dup = NULL;
struct transport_ls_refs_options transport_ls_refs_options =
TRANSPORT_LS_REFS_OPTIONS_INIT;
@ -955,6 +986,13 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
usage_msg_opt(_("You must specify a repository to clone."),
builtin_clone_usage, builtin_clone_options);
xsetenv("GIT_CLONE_PROTECTION_ACTIVE", "true", 0 /* allow user override */);
template_dir = get_template_dir(option_template);
if (*template_dir && !is_absolute_path(template_dir))
template_dir = template_dir_dup =
absolute_pathdup(template_dir);
xsetenv("GIT_CLONE_TEMPLATE_DIR", template_dir, 1);
if (option_depth || option_since || option_not.nr)
deepen = 1;
if (option_single_branch == -1)
@ -1116,7 +1154,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
* repository, and reference backends may persist that information into
* their on-disk data structures.
*/
init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN,
init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN,
ref_storage_format, NULL,
do_not_override_repo_unix_permissions, INIT_DB_QUIET | INIT_DB_SKIP_REFDB);
@ -1460,6 +1498,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
free(dir);
free(path);
free(repo_to_free);
free(template_dir_dup);
junk_mode = JUNK_LEAVE_ALL;
transport_ls_refs_options_release(&transport_ls_refs_options);

View file

@ -303,6 +303,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
struct child_process cp = CHILD_PROCESS_INIT;
char *displaypath;
if (validate_submodule_path(path) < 0)
exit(128);
displaypath = get_submodule_displaypath(path, info->prefix,
info->super_prefix);
@ -634,6 +637,9 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,
.free_removed_argv_elements = 1,
};
if (validate_submodule_path(path) < 0)
exit(128);
if (!submodule_from_path(the_repository, null_oid(), path))
die(_("no submodule mapping found in .gitmodules for path '%s'"),
path);
@ -1238,6 +1244,9 @@ static void sync_submodule(const char *path, const char *prefix,
if (!is_submodule_active(the_repository, path))
return;
if (validate_submodule_path(path) < 0)
exit(128);
sub = submodule_from_path(the_repository, null_oid(), path);
if (sub && sub->url) {
@ -1381,6 +1390,9 @@ static void deinit_submodule(const char *path, const char *prefix,
struct strbuf sb_config = STRBUF_INIT;
char *sub_git_dir = xstrfmt("%s/.git", path);
if (validate_submodule_path(path) < 0)
exit(128);
sub = submodule_from_path(the_repository, null_oid(), path);
if (!sub || !sub->name)
@ -1662,16 +1674,42 @@ static char *clone_submodule_sm_gitdir(const char *name)
return sm_gitdir;
}
static int dir_contains_only_dotgit(const char *path)
{
DIR *dir = opendir(path);
struct dirent *e;
int ret = 1;
if (!dir)
return 0;
e = readdir_skip_dot_and_dotdot(dir);
if (!e)
ret = 0;
else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
(e = readdir_skip_dot_and_dotdot(dir))) {
error("unexpected item '%s' in '%s'", e->d_name, path);
ret = 0;
}
closedir(dir);
return ret;
}
static int clone_submodule(const struct module_clone_data *clone_data,
struct string_list *reference)
{
char *p;
char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
char *sm_alternate = NULL, *error_strategy = NULL;
struct stat st;
struct child_process cp = CHILD_PROCESS_INIT;
const char *clone_data_path = clone_data->path;
char *to_free = NULL;
if (validate_submodule_path(clone_data_path) < 0)
exit(128);
if (!is_absolute_path(clone_data->path))
clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(),
clone_data->path);
@ -1681,6 +1719,10 @@ static int clone_submodule(const struct module_clone_data *clone_data,
"git dir"), sm_gitdir);
if (!file_exists(sm_gitdir)) {
if (clone_data->require_init && !stat(clone_data_path, &st) &&
!is_empty_dir(clone_data_path))
die(_("directory not empty: '%s'"), clone_data_path);
if (safe_create_leading_directories_const(sm_gitdir) < 0)
die(_("could not create directory '%s'"), sm_gitdir);
@ -1725,10 +1767,18 @@ static int clone_submodule(const struct module_clone_data *clone_data,
if(run_command(&cp))
die(_("clone of '%s' into submodule path '%s' failed"),
clone_data->url, clone_data_path);
if (clone_data->require_init && !stat(clone_data_path, &st) &&
!dir_contains_only_dotgit(clone_data_path)) {
char *dot_git = xstrfmt("%s/.git", clone_data_path);
unlink(dot_git);
free(dot_git);
die(_("directory not empty: '%s'"), clone_data_path);
}
} else {
char *path;
if (clone_data->require_init && !access(clone_data_path, X_OK) &&
if (clone_data->require_init && !stat(clone_data_path, &st) &&
!is_empty_dir(clone_data_path))
die(_("directory not empty: '%s'"), clone_data_path);
if (safe_create_leading_directories_const(clone_data_path) < 0)
@ -1738,6 +1788,23 @@ static int clone_submodule(const struct module_clone_data *clone_data,
free(path);
}
/*
* We already performed this check at the beginning of this function,
* before cloning the objects. This tries to detect racy behavior e.g.
* in parallel clones, where another process could easily have made the
* gitdir nested _after_ it was created.
*
* To prevent further harm coming from this unintentionally-nested
* gitdir, let's disable it by deleting the `HEAD` file.
*/
if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {
char *head = xstrfmt("%s/HEAD", sm_gitdir);
unlink(head);
free(head);
die(_("refusing to create/use '%s' in another submodule's "
"git dir"), sm_gitdir);
}
connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);
p = git_pathdup_submodule(clone_data_path, "config");
@ -2517,6 +2584,9 @@ static int update_submodule(struct update_data *update_data)
{
int ret;
if (validate_submodule_path(update_data->sm_path) < 0)
return -1;
ret = determine_submodule_update_strategy(the_repository,
update_data->just_cloned,
update_data->sm_path,
@ -2624,12 +2694,21 @@ static int update_submodules(struct update_data *update_data)
for (i = 0; i < suc.update_clone_nr; i++) {
struct update_clone_data ucd = suc.update_clone[i];
int code;
int code = 128;
oidcpy(&update_data->oid, &ucd.oid);
update_data->just_cloned = ucd.just_cloned;
update_data->sm_path = ucd.sub->path;
/*
* Verify that the submodule path does not contain any
* symlinks; if it does, it might have been tampered with.
* TODO: allow exempting it via
* `safe.submodule.path` or something
*/
if (validate_submodule_path(update_data->sm_path) < 0)
goto fail;
code = ensure_core_worktree(update_data->sm_path);
if (code)
goto fail;
@ -3356,6 +3435,9 @@ static int module_add(int argc, const char **argv, const char *prefix)
normalize_path_copy(add_data.sm_path, add_data.sm_path);
strip_dir_trailing_slashes(add_data.sm_path);
if (validate_submodule_path(add_data.sm_path) < 0)
exit(128);
die_on_index_match(add_data.sm_path, force);
die_on_repo_without_commits(add_data.sm_path);

View file

@ -37,6 +37,8 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
packet_trace_identity("upload-pack");
disable_replace_refs();
/* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
xsetenv("GIT_NO_LAZY_FETCH", "1", 0);
argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);

View file

@ -1411,8 +1411,19 @@ static int git_default_core_config(const char *var, const char *value,
if (!strcmp(var, "core.attributesfile"))
return git_config_pathname(&git_attributes_file, var, value);
if (!strcmp(var, "core.hookspath"))
if (!strcmp(var, "core.hookspath")) {
if (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&
git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0))
die(_("active `core.hooksPath` found in the local "
"repository config:\n\t%s\nFor security "
"reasons, this is disallowed by default.\nIf "
"this is intentional and the hook should "
"actually be run, please\nrun the command "
"again with "
"`GIT_CLONE_PROTECTION_ACTIVE=false`"),
value);
return git_config_pathname(&git_hooks_path, var, value);
}
if (!strcmp(var, "core.bare")) {
is_bare_repository_cfg = git_config_bool(var, value);

61
copy.c
View file

@ -1,6 +1,9 @@
#include "git-compat-util.h"
#include "copy.h"
#include "path.h"
#include "gettext.h"
#include "strbuf.h"
#include "abspath.h"
int copy_fd(int ifd, int ofd)
{
@ -67,3 +70,61 @@ int copy_file_with_time(const char *dst, const char *src, int mode)
return copy_times(dst, src);
return status;
}
static int do_symlinks_match(const char *path1, const char *path2)
{
struct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;
int ret = 0;
if (!strbuf_readlink(&buf1, path1, 0) &&
!strbuf_readlink(&buf2, path2, 0))
ret = !strcmp(buf1.buf, buf2.buf);
strbuf_release(&buf1);
strbuf_release(&buf2);
return ret;
}
int do_files_match(const char *path1, const char *path2)
{
struct stat st1, st2;
int fd1 = -1, fd2 = -1, ret = 1;
char buf1[8192], buf2[8192];
if ((fd1 = open_nofollow(path1, O_RDONLY)) < 0 ||
fstat(fd1, &st1) || !S_ISREG(st1.st_mode)) {
if (fd1 < 0 && errno == ELOOP)
/* maybe this is a symbolic link? */
return do_symlinks_match(path1, path2);
ret = 0;
} else if ((fd2 = open_nofollow(path2, O_RDONLY)) < 0 ||
fstat(fd2, &st2) || !S_ISREG(st2.st_mode)) {
ret = 0;
}
if (ret)
/* to match, neither must be executable, or both */
ret = !(st1.st_mode & 0111) == !(st2.st_mode & 0111);
if (ret)
ret = st1.st_size == st2.st_size;
while (ret) {
ssize_t len1 = read_in_full(fd1, buf1, sizeof(buf1));
ssize_t len2 = read_in_full(fd2, buf2, sizeof(buf2));
if (len1 < 0 || len2 < 0 || len1 != len2)
ret = 0; /* read error or different file size */
else if (!len1) /* len2 is also 0; hit EOF on both */
break; /* ret is still true */
else
ret = !memcmp(buf1, buf2, len1);
}
if (fd1 >= 0)
close(fd1);
if (fd2 >= 0)
close(fd2);
return ret;
}

14
copy.h
View file

@ -7,4 +7,18 @@ int copy_fd(int ifd, int ofd);
int copy_file(const char *dst, const char *src, int mode);
int copy_file_with_time(const char *dst, const char *src, int mode);
/*
* Compare the file mode and contents of two given files.
*
* If both files are actually symbolic links, the function returns 1 if the link
* targets are identical or 0 if they are not.
*
* If any of the two files cannot be accessed or in case of read failures, this
* function returns 0.
*
* If the file modes and contents are identical, the function returns 1,
* otherwise it returns 0.
*/
int do_files_match(const char *path1, const char *path2);
#endif /* COPY_H */

12
dir.c
View file

@ -100,6 +100,18 @@ int fspathncmp(const char *a, const char *b, size_t count)
return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
}
int paths_collide(const char *a, const char *b)
{
size_t len_a = strlen(a), len_b = strlen(b);
if (len_a == len_b)
return fspatheq(a, b);
if (len_a < len_b)
return is_dir_sep(b[len_a]) && !fspathncmp(a, b, len_a);
return is_dir_sep(a[len_b]) && !fspathncmp(a, b, len_b);
}
unsigned int fspathhash(const char *str)
{
return ignore_case ? strihash(str) : strhash(str);

7
dir.h
View file

@ -548,6 +548,13 @@ int fspatheq(const char *a, const char *b);
int fspathncmp(const char *a, const char *b, size_t count);
unsigned int fspathhash(const char *str);
/*
* Reports whether paths collide. This may be because the paths differ only in
* case on a case-sensitive filesystem, or that one path refers to a symlink
* that collides with one of the parent directories of the other.
*/
int paths_collide(const char *a, const char *b);
/*
* The prefix part of pattern must not contains wildcards.
*/

16
entry.c
View file

@ -460,7 +460,7 @@ static void mark_colliding_entries(const struct checkout *state,
continue;
if ((trust_ino && !match_stat_data(&dup->ce_stat_data, st)) ||
(!trust_ino && !fspathcmp(ce->name, dup->name))) {
paths_collide(ce->name, dup->name)) {
dup->ce_flags |= CE_MATCHED;
break;
}
@ -547,6 +547,20 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca,
/* If it is a gitlink, leave it alone! */
if (S_ISGITLINK(ce->ce_mode))
return 0;
/*
* We must avoid replacing submodules' leading
* directories with symbolic links, lest recursive
* clones can write into arbitrary locations.
*
* Technically, this logic is not limited
* to recursive clones, or for that matter to
* submodules' paths colliding with symbolic links'
* paths. Yet it strikes a balance in favor of
* simplicity, and if paths are colliding, we might
* just as well keep the directories during a clone.
*/
if (state->clone && S_ISLNK(ce->ce_mode))
return 0;
remove_subtree(&path);
} else if (unlink(path.buf))
return error_errno("unable to unlink old '%s'", path.buf);

56
fsck.c
View file

@ -656,6 +656,8 @@ static int fsck_tree(const struct object_id *tree_oid,
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_MAILMAP_SYMLINK,
".mailmap is a symlink");
oidset_insert(&options->symlink_targets_found,
entry_oid);
}
if ((backslash = strchr(name, '\\'))) {
@ -1164,6 +1166,56 @@ static int fsck_blob(const struct object_id *oid, const char *buf,
}
}
if (oidset_contains(&options->symlink_targets_found, oid)) {
const char *ptr = buf;
const struct object_id *reported = NULL;
oidset_insert(&options->symlink_targets_done, oid);
if (!buf || size > PATH_MAX) {
/*
* A missing buffer here is a sign that the caller found the
* blob too gigantic to load into memory. Let's just consider
* that an error.
*/
return report(options, oid, OBJ_BLOB,
FSCK_MSG_SYMLINK_TARGET_LENGTH,
"symlink target too long");
}
while (!reported && ptr) {
const char *p = ptr;
char c, *slash = strchrnul(ptr, '/');
char *backslash = memchr(ptr, '\\', slash - ptr);
c = *slash;
*slash = '\0';
while (!reported && backslash) {
*backslash = '\0';
if (is_ntfs_dotgit(p))
ret |= report(options, reported = oid, OBJ_BLOB,
FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
"symlink target points to git dir");
*backslash = '\\';
p = backslash + 1;
backslash = memchr(p, '\\', slash - p);
}
if (!reported && is_ntfs_dotgit(p))
ret |= report(options, reported = oid, OBJ_BLOB,
FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
"symlink target points to git dir");
if (!reported && is_hfs_dotgit(ptr))
ret |= report(options, reported = oid, OBJ_BLOB,
FSCK_MSG_SYMLINK_POINTS_TO_GIT_DIR,
"symlink target points to git dir");
*slash = c;
ptr = c ? slash + 1 : NULL;
}
}
return ret;
}
@ -1262,6 +1314,10 @@ int fsck_finish(struct fsck_options *options)
FSCK_MSG_GITATTRIBUTES_MISSING, FSCK_MSG_GITATTRIBUTES_BLOB,
options, ".gitattributes");
ret |= fsck_blobs(&options->symlink_targets_found, &options->symlink_targets_done,
FSCK_MSG_SYMLINK_TARGET_MISSING, FSCK_MSG_SYMLINK_TARGET_BLOB,
options, "<symlink-target>");
return ret;
}

12
fsck.h
View file

@ -64,6 +64,8 @@ enum fsck_msg_type {
FUNC(GITATTRIBUTES_LARGE, ERROR) \
FUNC(GITATTRIBUTES_LINE_LENGTH, ERROR) \
FUNC(GITATTRIBUTES_BLOB, ERROR) \
FUNC(SYMLINK_TARGET_MISSING, ERROR) \
FUNC(SYMLINK_TARGET_BLOB, ERROR) \
/* warnings */ \
FUNC(EMPTY_NAME, WARN) \
FUNC(FULL_PATHNAME, WARN) \
@ -74,6 +76,8 @@ enum fsck_msg_type {
FUNC(ZERO_PADDED_FILEMODE, WARN) \
FUNC(NUL_IN_COMMIT, WARN) \
FUNC(LARGE_PATHNAME, WARN) \
FUNC(SYMLINK_TARGET_LENGTH, WARN) \
FUNC(SYMLINK_POINTS_TO_GIT_DIR, WARN) \
/* infos (reported as warnings, but ignored by default) */ \
FUNC(BAD_FILEMODE, INFO) \
FUNC(GITMODULES_PARSE, INFO) \
@ -141,6 +145,8 @@ struct fsck_options {
struct oidset gitmodules_done;
struct oidset gitattributes_found;
struct oidset gitattributes_done;
struct oidset symlink_targets_found;
struct oidset symlink_targets_done;
kh_oid_map_t *object_names;
};
@ -150,6 +156,8 @@ struct fsck_options {
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
.symlink_targets_found = OIDSET_INIT, \
.symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_function \
}
#define FSCK_OPTIONS_STRICT { \
@ -158,6 +166,8 @@ struct fsck_options {
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
.symlink_targets_found = OIDSET_INIT, \
.symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_function, \
}
#define FSCK_OPTIONS_MISSING_GITMODULES { \
@ -166,6 +176,8 @@ struct fsck_options {
.gitmodules_done = OIDSET_INIT, \
.gitattributes_found = OIDSET_INIT, \
.gitattributes_done = OIDSET_INIT, \
.symlink_targets_found = OIDSET_INIT, \
.symlink_targets_done = OIDSET_INIT, \
.error_func = fsck_error_cb_print_missing_gitmodules, \
}

View file

@ -126,6 +126,15 @@
#define GIT_CURL_HAVE_CURLSSLSET_NO_BACKENDS
#endif
/**
* Versions before curl 7.66.0 (September 2019) required manually setting the
* transfer-encoding for a streaming POST; after that this is handled
* automatically.
*/
#if LIBCURL_VERSION_NUM < 0x074200
#define GIT_CURL_NEED_TRANSFER_ENCODING_HEADER
#endif
/**
* CURLOPT_PROTOCOLS_STR and CURLOPT_REDIR_PROTOCOLS_STR were added in 7.85.0,
* released in August 2022.

53
hook.c
View file

@ -7,25 +7,56 @@
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
#include "environment.h"
#include "setup.h"
#include "copy.h"
static int identical_to_template_hook(const char *name, const char *path)
{
const char *env = getenv("GIT_CLONE_TEMPLATE_DIR");
const char *template_dir = get_template_dir(env && *env ? env : NULL);
struct strbuf template_path = STRBUF_INIT;
int found_template_hook, ret;
strbuf_addf(&template_path, "%s/hooks/%s", template_dir, name);
found_template_hook = access(template_path.buf, X_OK) >= 0;
#ifdef STRIP_EXTENSION
if (!found_template_hook) {
strbuf_addstr(&template_path, STRIP_EXTENSION);
found_template_hook = access(template_path.buf, X_OK) >= 0;
}
#endif
if (!found_template_hook)
return 0;
ret = do_files_match(template_path.buf, path);
strbuf_release(&template_path);
return ret;
}
const char *find_hook(const char *name)
{
static struct strbuf path = STRBUF_INIT;
int found_hook;
strbuf_reset(&path);
strbuf_git_path(&path, "hooks/%s", name);
if (access(path.buf, X_OK) < 0) {
found_hook = access(path.buf, X_OK) >= 0;
#ifdef STRIP_EXTENSION
if (!found_hook) {
int err = errno;
#ifdef STRIP_EXTENSION
strbuf_addstr(&path, STRIP_EXTENSION);
if (access(path.buf, X_OK) >= 0)
return path.buf;
if (errno == EACCES)
err = errno;
found_hook = access(path.buf, X_OK) >= 0;
if (!found_hook)
errno = err;
}
#endif
if (err == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
if (!found_hook) {
if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
static struct string_list advise_given = STRING_LIST_INIT_DUP;
if (!string_list_lookup(&advise_given, name)) {
@ -39,6 +70,14 @@ const char *find_hook(const char *name)
}
return NULL;
}
if (!git_hooks_path && git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0) &&
!identical_to_template_hook(name, path.buf))
die(_("active `%s` hook found during `git clone`:\n\t%s\n"
"For security reasons, this is disallowed by default.\n"
"If this is intentional and the hook should actually "
"be run, please\nrun the command again with "
"`GIT_CLONE_PROTECTION_ACTIVE=false`"),
name, path.buf);
return path.buf;
}

1
http.c
View file

@ -1452,6 +1452,7 @@ struct active_request_slot *get_active_slot(void)
curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, NULL);
curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, NULL);
curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDS, NULL);
curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDSIZE, -1L);
curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 0);
curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, 1);

2
path.c
View file

@ -846,6 +846,7 @@ const char *enter_repo(const char *path, int strict)
if (!suffix[i])
return NULL;
gitfile = read_gitfile(used_path.buf);
die_upon_dubious_ownership(gitfile, NULL, used_path.buf);
if (gitfile) {
strbuf_reset(&used_path);
strbuf_addstr(&used_path, gitfile);
@ -856,6 +857,7 @@ const char *enter_repo(const char *path, int strict)
}
else {
const char *gitfile = read_gitfile(path);
die_upon_dubious_ownership(gitfile, NULL, path);
if (gitfile)
path = gitfile;
if (chdir(path))

View file

@ -23,6 +23,16 @@ static int fetch_objects(struct repository *repo,
int i;
FILE *child_in;
/* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
if (git_env_bool("GIT_NO_LAZY_FETCH", 0)) {
static int warning_shown;
if (!warning_shown) {
warning_shown = 1;
warning(_("lazy fetching disabled; some objects may not be available"));
}
return -1;
}
child.git_cmd = 1;
child.in = -1;
if (repo != the_repository)

View file

@ -1116,19 +1116,32 @@ static int has_dir_name(struct index_state *istate,
istate->cache[istate->cache_nr - 1]->name,
&len_eq_last);
if (cmp_last > 0) {
if (len_eq_last == 0) {
if (name[len_eq_last] != '/') {
/*
* The entry sorts AFTER the last one in the
* index and their paths have no common prefix,
* so there cannot be a F/D conflict.
* index.
*
* If there were a conflict with "file", then our
* name would start with "file/" and the last index
* entry would start with "file" but not "file/".
*
* The next character after common prefix is
* not '/', so there can be no conflict.
*/
return retval;
} else {
/*
* The entry sorts AFTER the last one in the
* index, but has a common prefix. Fall through
* to the loop below to disect the entry's path
* and see where the difference is.
* index, and the next character after common
* prefix is '/'.
*
* Either the last index entry is a file in
* conflict with this entry, or it has a name
* which sorts between this entry and the
* potential conflicting file.
*
* In both cases, we fall through to the loop
* below and let the regular search code handle it.
*/
}
} else if (cmp_last == 0) {
@ -1152,53 +1165,6 @@ static int has_dir_name(struct index_state *istate,
}
len = slash - name;
if (cmp_last > 0) {
/*
* (len + 1) is a directory boundary (including
* the trailing slash). And since the loop is
* decrementing "slash", the first iteration is
* the longest directory prefix; subsequent
* iterations consider parent directories.
*/
if (len + 1 <= len_eq_last) {
/*
* The directory prefix (including the trailing
* slash) also appears as a prefix in the last
* entry, so the remainder cannot collide (because
* strcmp said the whole path was greater).
*
* EQ: last: xxx/A
* this: xxx/B
*
* LT: last: xxx/file_A
* this: xxx/file_B
*/
return retval;
}
if (len > len_eq_last) {
/*
* This part of the directory prefix (excluding
* the trailing slash) is longer than the known
* equal portions, so this sub-directory cannot
* collide with a file.
*
* GT: last: xxxA
* this: xxxB/file
*/
return retval;
}
/*
* This is a possible collision. Fall through and
* let the regular search code handle it.
*
* last: xxx
* this: xxx/file
*/
}
pos = index_name_stage_pos(istate, name, len, stage, EXPAND_SPARSE);
if (pos >= 0) {
/*

View file

@ -1,4 +1,5 @@
#include "git-compat-util.h"
#include "git-curl-compat.h"
#include "config.h"
#include "environment.h"
#include "gettext.h"
@ -960,7 +961,9 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece
/* The request body is large and the size cannot be predicted.
* We must use chunked encoding to send it.
*/
#ifdef GIT_CURL_NEED_TRANSFER_ENCODING_HEADER
headers = curl_slist_append(headers, "Transfer-Encoding: chunked");
#endif
rpc->initial_buffer = 1;
curl_easy_setopt(slot->curl, CURLOPT_READFUNCTION, rpc_out);
curl_easy_setopt(slot->curl, CURLOPT_INFILE, rpc);

View file

@ -281,6 +281,8 @@ void repo_clear(struct repository *repo)
parsed_object_pool_clear(repo->parsed_objects);
FREE_AND_NULL(repo->parsed_objects);
FREE_AND_NULL(repo->settings.fsmonitor);
if (repo->config) {
git_configset_clear(repo->config);
FREE_AND_NULL(repo->config);

91
setup.c
View file

@ -16,6 +16,7 @@
#include "quote.h"
#include "trace2.h"
#include "worktree.h"
#include "exec-cmd.h"
static int inside_git_dir = -1;
static int inside_work_tree = -1;
@ -1201,6 +1202,27 @@ static int ensure_valid_ownership(const char *gitfile,
return data.is_safe;
}
void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
const char *gitdir)
{
struct strbuf report = STRBUF_INIT, quoted = STRBUF_INIT;
const char *path;
if (ensure_valid_ownership(gitfile, worktree, gitdir, &report))
return;
strbuf_complete(&report, '\n');
path = gitfile ? gitfile : gitdir;
sq_quote_buf_pretty(&quoted, path);
die(_("detected dubious ownership in repository at '%s'\n"
"%s"
"To add an exception for this directory, call:\n"
"\n"
"\tgit config --global --add safe.directory %s"),
path, report.buf, quoted.buf);
}
static int allowed_bare_repo_cb(const char *key, const char *value,
const struct config_context *ctx UNUSED,
void *d)
@ -1733,6 +1755,57 @@ int daemonize(void)
#endif
}
struct template_dir_cb_data {
char *path;
int initialized;
};
static int template_dir_cb(const char *key, const char *value,
const struct config_context *ctx, void *d)
{
struct template_dir_cb_data *data = d;
if (strcmp(key, "init.templatedir"))
return 0;
if (!value) {
data->path = NULL;
} else {
char *path = NULL;
FREE_AND_NULL(data->path);
if (!git_config_pathname((const char **)&path, key, value))
data->path = path ? path : xstrdup(value);
}
return 0;
}
const char *get_template_dir(const char *option_template)
{
const char *template_dir = option_template;
if (!template_dir)
template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
if (!template_dir) {
static struct template_dir_cb_data data;
if (!data.initialized) {
git_protected_config(template_dir_cb, &data);
data.initialized = 1;
}
template_dir = data.path;
}
if (!template_dir) {
static char *dir;
if (!dir)
dir = system_path(DEFAULT_GIT_TEMPLATE_DIR);
template_dir = dir;
}
return template_dir;
}
#ifdef NO_TRUSTABLE_FILEMODE
#define TEST_FILEMODE 0
#else
@ -1808,8 +1881,9 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
}
}
static void copy_templates(const char *template_dir, const char *init_template_dir)
static void copy_templates(const char *option_template)
{
const char *template_dir = get_template_dir(option_template);
struct strbuf path = STRBUF_INIT;
struct strbuf template_path = STRBUF_INIT;
size_t template_len;
@ -1818,16 +1892,8 @@ static void copy_templates(const char *template_dir, const char *init_template_d
DIR *dir;
char *to_free = NULL;
if (!template_dir)
template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
if (!template_dir)
template_dir = init_template_dir;
if (!template_dir)
template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR);
if (!template_dir[0]) {
free(to_free);
if (!template_dir || !*template_dir)
return;
}
strbuf_addstr(&template_path, template_dir);
strbuf_complete(&template_path, '/');
@ -1969,7 +2035,6 @@ static int create_default_files(const char *template_path,
char *path;
int reinit;
int filemode;
const char *init_template_dir = NULL;
const char *work_tree = get_git_work_tree();
/*
@ -1981,9 +2046,7 @@ static int create_default_files(const char *template_path,
* values (since we've just potentially changed what's available on
* disk).
*/
git_config_get_pathname("init.templatedir", &init_template_dir);
copy_templates(template_path, init_template_dir);
free((char *)init_template_dir);
copy_templates(template_path);
git_config_clear();
reset_shared_repository();
git_config(git_default_config, NULL);

14
setup.h
View file

@ -41,6 +41,18 @@ const char *read_gitfile_gently(const char *path, int *return_error_code);
const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);
#define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)
/*
* Check if a repository is safe and die if it is not, by verifying the
* ownership of the worktree (if any), the git directory, and the gitfile (if
* any).
*
* Exemptions for known-safe repositories can be added via `safe.directory`
* config settings; for non-bare repositories, their worktree needs to be
* added, for bare ones their git directory.
*/
void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
const char *gitdir);
void setup_work_tree(void);
/*
@ -171,6 +183,8 @@ int verify_repository_format(const struct repository_format *format,
*/
void check_repository_format(struct repository_format *fmt);
const char *get_template_dir(const char *option_template);
#define INIT_DB_QUIET (1 << 0)
#define INIT_DB_EXIST_OK (1 << 1)
#define INIT_DB_SKIP_REFDB (1 << 2)

View file

@ -1010,6 +1010,9 @@ static int submodule_has_commits(struct repository *r,
.super_oid = super_oid
};
if (validate_submodule_path(path) < 0)
exit(128);
oid_array_for_each_unique(commits, check_has_commit, &has_commit);
if (has_commit.result) {
@ -1132,6 +1135,9 @@ static int push_submodule(const char *path,
const struct string_list *push_options,
int dry_run)
{
if (validate_submodule_path(path) < 0)
exit(128);
if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
struct child_process cp = CHILD_PROCESS_INIT;
strvec_push(&cp.args, "push");
@ -1181,6 +1187,9 @@ static void submodule_push_check(const char *path, const char *head,
struct child_process cp = CHILD_PROCESS_INIT;
int i;
if (validate_submodule_path(path) < 0)
exit(128);
strvec_push(&cp.args, "submodule--helper");
strvec_push(&cp.args, "push-check");
strvec_push(&cp.args, head);
@ -1512,6 +1521,9 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
struct fetch_task *task = xmalloc(sizeof(*task));
memset(task, 0, sizeof(*task));
if (validate_submodule_path(path) < 0)
exit(128);
task->sub = submodule_from_path(spf->r, treeish_name, path);
if (!task->sub) {
@ -1884,6 +1896,9 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
const char *git_dir;
int ignore_cp_exit_code = 0;
if (validate_submodule_path(path) < 0)
exit(128);
strbuf_addf(&buf, "%s/.git", path);
git_dir = read_gitfile(buf.buf);
if (!git_dir)
@ -1960,6 +1975,9 @@ int submodule_uses_gitfile(const char *path)
struct strbuf buf = STRBUF_INIT;
const char *git_dir;
if (validate_submodule_path(path) < 0)
exit(128);
strbuf_addf(&buf, "%s/.git", path);
git_dir = read_gitfile(buf.buf);
if (!git_dir) {
@ -1999,6 +2017,9 @@ int bad_to_remove_submodule(const char *path, unsigned flags)
struct strbuf buf = STRBUF_INIT;
int ret = 0;
if (validate_submodule_path(path) < 0)
exit(128);
if (!file_exists(path) || is_empty_dir(path))
return 0;
@ -2049,6 +2070,9 @@ void submodule_unset_core_worktree(const struct submodule *sub)
{
struct strbuf config_path = STRBUF_INIT;
if (validate_submodule_path(sub->path) < 0)
exit(128);
submodule_name_to_gitdir(&config_path, the_repository, sub->name);
strbuf_addstr(&config_path, "/config");
@ -2063,6 +2087,9 @@ static int submodule_has_dirty_index(const struct submodule *sub)
{
struct child_process cp = CHILD_PROCESS_INIT;
if (validate_submodule_path(sub->path) < 0)
exit(128);
prepare_submodule_repo_env(&cp.env);
cp.git_cmd = 1;
@ -2080,6 +2107,10 @@ static int submodule_has_dirty_index(const struct submodule *sub)
static void submodule_reset_index(const char *path, const char *super_prefix)
{
struct child_process cp = CHILD_PROCESS_INIT;
if (validate_submodule_path(path) < 0)
exit(128);
prepare_submodule_repo_env(&cp.env);
cp.git_cmd = 1;
@ -2143,10 +2174,27 @@ int submodule_move_head(const char *path, const char *super_prefix,
if (!submodule_uses_gitfile(path))
absorb_git_dir_into_superproject(path,
super_prefix);
else {
char *dotgit = xstrfmt("%s/.git", path);
char *git_dir = xstrdup(read_gitfile(dotgit));
free(dotgit);
if (validate_submodule_git_dir(git_dir,
sub->name) < 0)
die(_("refusing to create/use '%s' in "
"another submodule's git dir"),
git_dir);
free(git_dir);
}
} else {
struct strbuf gitdir = STRBUF_INIT;
submodule_name_to_gitdir(&gitdir, the_repository,
sub->name);
if (validate_submodule_git_dir(gitdir.buf,
sub->name) < 0)
die(_("refusing to create/use '%s' in another "
"submodule's git dir"),
gitdir.buf);
connect_work_tree_and_git_dir(path, gitdir.buf, 0);
strbuf_release(&gitdir);
@ -2267,6 +2315,34 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
return 0;
}
int validate_submodule_path(const char *path)
{
char *p = xstrdup(path);
struct stat st;
int i, ret = 0;
char sep;
for (i = 0; !ret && p[i]; i++) {
if (!is_dir_sep(p[i]))
continue;
sep = p[i];
p[i] = '\0';
/* allow missing components, but no symlinks */
ret = lstat(p, &st) || !S_ISLNK(st.st_mode) ? 0 : -1;
p[i] = sep;
if (ret)
error(_("expected '%.*s' in submodule path '%s' not to "
"be a symbolic link"), i, p, p);
}
if (!lstat(p, &st) && S_ISLNK(st.st_mode))
ret = error(_("expected submodule path '%s' not to be a "
"symbolic link"), p);
free(p);
return ret;
}
/*
* Embeds a single submodules git directory into the superprojects git dir,
* non recursively.
@ -2278,6 +2354,9 @@ static void relocate_single_git_dir_into_superproject(const char *path,
struct strbuf new_gitdir = STRBUF_INIT;
const struct submodule *sub;
if (validate_submodule_path(path) < 0)
exit(128);
if (submodule_uses_worktrees(path))
die(_("relocate_gitdir for submodule '%s' with "
"more than one worktree not supported"), path);
@ -2319,6 +2398,9 @@ static void absorb_git_dir_into_superproject_recurse(const char *path,
struct child_process cp = CHILD_PROCESS_INIT;
if (validate_submodule_path(path) < 0)
exit(128);
cp.dir = path;
cp.git_cmd = 1;
cp.no_stdin = 1;
@ -2343,6 +2425,10 @@ void absorb_git_dir_into_superproject(const char *path,
int err_code;
const char *sub_git_dir;
struct strbuf gitdir = STRBUF_INIT;
if (validate_submodule_path(path) < 0)
exit(128);
strbuf_addf(&gitdir, "%s/.git", path);
sub_git_dir = resolve_gitdir_gently(gitdir.buf, &err_code);
@ -2485,6 +2571,9 @@ int submodule_to_gitdir(struct strbuf *buf, const char *submodule)
const char *git_dir;
int ret = 0;
if (validate_submodule_path(submodule) < 0)
exit(128);
strbuf_reset(buf);
strbuf_addstr(buf, submodule);
strbuf_complete(buf, '/');

View file

@ -148,6 +148,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
*/
int validate_submodule_git_dir(char *git_dir, const char *submodule_name);
/*
* Make sure that the given submodule path does not follow symlinks.
*/
int validate_submodule_path(const char *path);
#define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0)
#define SUBMODULE_MOVE_HEAD_FORCE (1<<1)
int submodule_move_head(const char *path, const char *super_prefix,

View file

@ -7,6 +7,7 @@
#include "string-list.h"
#include "trace.h"
#include "utf8.h"
#include "copy.h"
/*
* A "string_list_each_func_t" function that normalizes an entry from
@ -500,6 +501,16 @@ int cmd__path_utils(int argc, const char **argv)
return !!res;
}
if (argc == 4 && !strcmp(argv[1], "do_files_match")) {
int ret = do_files_match(argv[2], argv[3]);
if (ret)
printf("equal\n");
else
printf("different\n");
return !ret;
}
fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
argv[1] ? argv[1] : "(there was none)");
return 1;

View file

@ -1201,6 +1201,34 @@ test_expect_success 'very long name in the index handled sanely' '
test $len = 4098
'
# D/F conflict checking uses an optimization when adding to the end.
# make sure it does not get confused by `a-` sorting _between_
# `a` and `a/`.
test_expect_success 'more update-index D/F conflicts' '
# empty the index to make sure our entry is last
git read-tree --empty &&
cacheinfo=100644,$(test_oid empty_blob) &&
git update-index --add --cacheinfo $cacheinfo,path5/a &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
# "a-" sorts between "a" and "a/"
git update-index --add --cacheinfo $cacheinfo,path5/a- &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
cat >expected <<-\EOF &&
path5/a
path5/a-
EOF
git ls-files >actual &&
test_cmp expected actual
'
test_expect_success 'test_must_fail on a failing git command' '
test_must_fail git notacommand
'

View file

@ -80,4 +80,28 @@ test_expect_success 'safe.directory in included file' '
git status
'
test_expect_success 'local clone of unowned repo refused in unsafe directory' '
test_when_finished "rm -rf source" &&
git init source &&
(
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
test_commit -C source initial
) &&
test_must_fail git clone --local source target &&
test_path_is_missing target
'
test_expect_success 'local clone of unowned repo accepted in safe directory' '
test_when_finished "rm -rf source" &&
git init source &&
(
sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
test_commit -C source initial
) &&
test_must_fail git clone --local source target &&
git config --global --add safe.directory "$(pwd)/source/.git" &&
git clone --local source target &&
test_path_is_dir target
'
test_done

View file

@ -610,4 +610,45 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'
test_cmp expect actual
'
test_expect_success 'do_files_match()' '
test_seq 0 10 >0-10.txt &&
test_seq -1 10 >-1-10.txt &&
test_seq 1 10 >1-10.txt &&
test_seq 1 9 >1-9.txt &&
test_seq 0 8 >0-8.txt &&
test-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&
assert_fails() {
test_must_fail \
test-tool path-utils do_files_match "$1" "$2" >out &&
grep different out
} &&
assert_fails 0-8.txt 1-9.txt &&
assert_fails -1-10.txt 0-10.txt &&
assert_fails 1-10.txt 1-9.txt &&
assert_fails 1-10.txt .git &&
assert_fails does-not-exist 1-10.txt &&
if test_have_prereq FILEMODE
then
cp 0-10.txt 0-10.x &&
chmod a+x 0-10.x &&
assert_fails 0-10.txt 0-10.x
fi &&
if test_have_prereq SYMLINKS
then
ln -sf 0-10.txt symlink &&
ln -s 0-10.txt another-symlink &&
ln -s over-the-ocean yet-another-symlink &&
ln -s "$PWD/0-10.txt" absolute-symlink &&
assert_fails 0-10.txt symlink &&
test-tool path-utils do_files_match symlink another-symlink &&
assert_fails symlink yet-another-symlink &&
assert_fails symlink absolute-symlink
fi
'
test_done

78
t/t0411-clone-from-partial.sh Executable file
View file

@ -0,0 +1,78 @@
#!/bin/sh
test_description='check that local clone does not fetch from promisor remotes'
. ./test-lib.sh
test_expect_success 'create evil repo' '
git init tmp &&
test_commit -C tmp a &&
git -C tmp config uploadpack.allowfilter 1 &&
git clone --filter=blob:none --no-local --no-checkout tmp evil &&
rm -rf tmp &&
git -C evil config remote.origin.uploadpack \"\$TRASH_DIRECTORY/fake-upload-pack\" &&
write_script fake-upload-pack <<-\EOF &&
echo >&2 "fake-upload-pack running"
>"$TRASH_DIRECTORY/script-executed"
exit 1
EOF
export TRASH_DIRECTORY &&
# empty shallow file disables local clone optimization
>evil/.git/shallow
'
test_expect_success 'local clone must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git clone \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
evil clone1 2>err &&
test_grep "detected dubious ownership" err &&
test_grep ! "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'clone from file://... must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git clone \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
"file://$(pwd)/evil" clone2 2>err &&
test_grep "detected dubious ownership" err &&
test_grep ! "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'fetch from file://... must not fetch from promisor remote and execute script' '
rm -f script-executed &&
test_must_fail git fetch \
--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
"file://$(pwd)/evil" 2>err &&
test_grep "detected dubious ownership" err &&
test_grep ! "fake-upload-pack running" err &&
test_path_is_missing script-executed
'
test_expect_success 'pack-objects should fetch from promisor remote and execute script' '
rm -f script-executed &&
echo "HEAD" | test_must_fail git -C evil pack-objects --revs --stdout >/dev/null 2>err &&
test_grep "fake-upload-pack running" err &&
test_path_is_file script-executed
'
test_expect_success 'clone from promisor remote does not lazy-fetch by default' '
rm -f script-executed &&
test_must_fail git clone evil no-lazy 2>err &&
test_grep "lazy fetching disabled" err &&
test_path_is_missing script-executed
'
test_expect_success 'promisor lazy-fetching can be re-enabled' '
rm -f script-executed &&
test_must_fail env GIT_NO_LAZY_FETCH=0 \
git clone evil lazy-ok 2>err &&
test_grep "fake-upload-pack running" err &&
test_path_is_file script-executed
'
test_done

View file

@ -1060,4 +1060,41 @@ test_expect_success 'fsck reports problems in current worktree index without fil
test_cmp expect actual
'
test_expect_success 'fsck warning on symlink target with excessive length' '
symlink_target=$(printf "pattern %032769d" 1 | git hash-object -w --stdin) &&
test_when_finished "remove_object $symlink_target" &&
tree=$(printf "120000 blob %s\t%s\n" $symlink_target symlink | git mktree) &&
test_when_finished "remove_object $tree" &&
cat >expected <<-EOF &&
warning in blob $symlink_target: symlinkTargetLength: symlink target too long
EOF
git fsck --no-dangling >actual 2>&1 &&
test_cmp expected actual
'
test_expect_success 'fsck warning on symlink target pointing inside git dir' '
gitdir=$(printf ".git" | git hash-object -w --stdin) &&
ntfs_gitdir=$(printf "GIT~1" | git hash-object -w --stdin) &&
hfs_gitdir=$(printf ".${u200c}git" | git hash-object -w --stdin) &&
inside_gitdir=$(printf "nested/.git/config" | git hash-object -w --stdin) &&
benign_target=$(printf "legit/config" | git hash-object -w --stdin) &&
tree=$(printf "120000 blob %s\t%s\n" \
$benign_target benign_target \
$gitdir gitdir \
$hfs_gitdir hfs_gitdir \
$inside_gitdir inside_gitdir \
$ntfs_gitdir ntfs_gitdir |
git mktree) &&
for o in $gitdir $ntfs_gitdir $hfs_gitdir $inside_gitdir $benign_target $tree
do
test_when_finished "remove_object $o" || return 1
done &&
printf "warning in blob %s: symlinkPointsToGitDir: symlink target points to git dir\n" \
$gitdir $hfs_gitdir $inside_gitdir $ntfs_gitdir |
sort >expected &&
git fsck --no-dangling >actual 2>&1 &&
sort actual >actual.sorted &&
test_cmp expected actual.sorted
'
test_done

View file

@ -185,4 +185,19 @@ test_expect_success 'stdin to hooks' '
test_cmp expect actual
'
test_expect_success 'clone protections' '
test_config core.hooksPath "$(pwd)/my-hooks" &&
mkdir -p my-hooks &&
write_script my-hooks/test-hook <<-\EOF &&
echo Hook ran $1
EOF
git hook run test-hook 2>err &&
test_grep "Hook ran" err &&
test_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \
git hook run test-hook 2>err &&
test_grep "active .core.hooksPath" err &&
test_grep ! "Hook ran" err
'
test_done

View file

@ -1252,6 +1252,30 @@ EOF
test_cmp fatal-expect fatal-actual
'
test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
git init df-conflict &&
(
cd df-conflict &&
ln -s .git a &&
git add a &&
test_tick &&
git commit -m symlink &&
test_commit a- &&
rm a &&
mkdir -p a/hooks &&
write_script a/hooks/post-checkout <<-EOF &&
echo WHOOPSIE >&2
echo whoopsie >"$TRASH_DIRECTORY"/whoops
EOF
git add a/hooks/post-checkout &&
test_tick &&
git commit -m post-checkout
) &&
git clone df-conflict clone 2>err &&
test_grep ! WHOOPS err &&
test_path_is_missing whoops
'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd

View file

@ -650,6 +650,21 @@ test_expect_success CASE_INSENSITIVE_FS 'colliding file detection' '
test_grep "the following paths have collided" icasefs/warning
'
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
'colliding symlink/directory keeps directory' '
git init icasefs-colliding-symlink &&
(
cd icasefs-colliding-symlink &&
a=$(printf a | git hash-object -w --stdin) &&
printf "100644 %s 0\tA/dir/b\n120000 %s 0\ta\n" $a $a >idx &&
git update-index --index-info <idx &&
test_tick &&
git commit -m initial
) &&
git clone icasefs-colliding-symlink icasefs-colliding-symlink-clone &&
test_file_not_empty icasefs-colliding-symlink-clone/A/dir/b
'
test_expect_success 'clone with GIT_DEFAULT_HASH' '
(
sane_unset GIT_DEFAULT_HASH &&
@ -773,6 +788,57 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe
git clone --filter=blob:limit=0 "file://$(pwd)/server" client
'
test_expect_success 'clone with init.templatedir runs hooks' '
git init tmpl/hooks &&
write_script tmpl/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo I was here >hook.run
EOF
git -C tmpl/hooks add . &&
test_tick &&
git -C tmpl/hooks commit -m post-checkout &&
test_when_finished "git config --global --unset init.templateDir || :" &&
test_when_finished "git config --unset init.templateDir || :" &&
(
sane_unset GIT_TEMPLATE_DIR &&
NO_SET_GIT_TEMPLATE_DIR=t &&
export NO_SET_GIT_TEMPLATE_DIR &&
git -c core.hooksPath="$(pwd)/tmpl/hooks" \
clone tmpl/hooks hook-run-hookspath 2>err &&
test_grep ! "active .* hook found" err &&
test_path_is_file hook-run-hookspath/hook.run &&
git -c init.templateDir="$(pwd)/tmpl" \
clone tmpl/hooks hook-run-config 2>err &&
test_grep ! "active .* hook found" err &&
test_path_is_file hook-run-config/hook.run &&
git clone --template=tmpl tmpl/hooks hook-run-option 2>err &&
test_grep ! "active .* hook found" err &&
test_path_is_file hook-run-option/hook.run &&
git config --global init.templateDir "$(pwd)/tmpl" &&
git clone tmpl/hooks hook-run-global-config 2>err &&
git config --global --unset init.templateDir &&
test_grep ! "active .* hook found" err &&
test_path_is_file hook-run-global-config/hook.run &&
# clone ignores local `init.templateDir`; need to create
# a new repository because we deleted `.git/` in the
# `setup` test case above
git init local-clone &&
cd local-clone &&
git config init.templateDir "$(pwd)/../tmpl" &&
git clone ../tmpl/hooks hook-run-local-config 2>err &&
git config --unset init.templateDir &&
test_grep ! "active .* hook found" err &&
test_path_is_missing hook-run-local-config/hook.run
)
'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd

View file

@ -1452,4 +1452,35 @@ test_expect_success 'recursive clone respects -q' '
test_must_be_empty actual
'
test_expect_success '`submodule init` and `init.templateDir`' '
mkdir -p tmpl/hooks &&
write_script tmpl/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo I was here >hook.run
exit 1
EOF
test_config init.templateDir "$(pwd)/tmpl" &&
test_when_finished \
"git config --global --unset init.templateDir || true" &&
(
sane_unset GIT_TEMPLATE_DIR &&
NO_SET_GIT_TEMPLATE_DIR=t &&
export NO_SET_GIT_TEMPLATE_DIR &&
git config --global init.templateDir "$(pwd)/tmpl" &&
test_must_fail git submodule \
add "$submodurl" sub-global 2>err &&
git config --global --unset init.templateDir &&
test_grep HOOK-RUN err &&
test_path_is_file sub-global/hook.run &&
git config init.templateDir "$(pwd)/tmpl" &&
git submodule add "$submodurl" sub-local 2>err &&
git config --unset init.templateDir &&
test_grep ! HOOK-RUN err &&
test_path_is_missing sub-local/hook.run
)
'
test_done

View file

@ -1202,4 +1202,52 @@ test_expect_success 'commit with staged submodule change with ignoreSubmodules a
add_submodule_commit_and_validate
'
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
'submodule paths must not follow symlinks' '
# This is only needed because we want to run this in a self-contained
# test without having to spin up an HTTP server; However, it would not
# be needed in a real-world scenario where the submodule is simply
# hosted on a public site.
test_config_global protocol.file.allow always &&
# Make sure that Git tries to use symlinks on Windows
test_config_global core.symlinks true &&
tell_tale_path="$PWD/tell.tale" &&
git init hook &&
(
cd hook &&
mkdir -p y/hooks &&
write_script y/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo hook-run >"$tell_tale_path"
EOF
git add y/hooks/post-checkout &&
test_tick &&
git commit -m post-checkout
) &&
hook_repo_path="$(pwd)/hook" &&
git init captain &&
(
cd captain &&
git submodule add --name x/y "$hook_repo_path" A/modules/x &&
test_tick &&
git commit -m add-submodule &&
printf .git >dotgit.txt &&
git hash-object -w --stdin <dotgit.txt >dot-git.hash &&
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info &&
git update-index --index-info <index.info &&
test_tick &&
git commit -m add-symlink
) &&
test_path_is_missing "$tell_tale_path" &&
git clone --recursive captain hooked 2>err &&
test_grep ! HOOK-RUN err &&
test_path_is_missing "$tell_tale_path"
'
test_done

67
t/t7423-submodule-symlinks.sh Executable file
View file

@ -0,0 +1,67 @@
#!/bin/sh
test_description='check that submodule operations do not follow symlinks'
. ./test-lib.sh
test_expect_success 'prepare' '
git config --global protocol.file.allow always &&
test_commit initial &&
git init upstream &&
test_commit -C upstream upstream submodule_file &&
git submodule add ./upstream a/sm &&
test_tick &&
git commit -m submodule
'
test_expect_success SYMLINKS 'git submodule update must not create submodule behind symlink' '
rm -rf a b &&
mkdir b &&
ln -s b a &&
test_path_is_missing b/sm &&
test_must_fail git submodule update &&
test_path_is_missing b/sm
'
test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' '
rm -rf a b &&
mkdir b &&
ln -s b A &&
test_must_fail git submodule update &&
test_path_is_missing b/sm
'
prepare_symlink_to_repo() {
rm -rf a &&
mkdir a &&
git init a/target &&
git -C a/target fetch ../../upstream &&
ln -s target a/sm
}
test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confused by a symlink' '
prepare_symlink_to_repo &&
test_must_fail git restore --recurse-submodules a/sm &&
test_path_is_missing a/sm/submodule_file &&
test_path_is_dir a/target/.git &&
test_path_is_missing a/target/submodule_file
'
test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '
prepare_symlink_to_repo &&
rm -rf .git/modules &&
test_must_fail git restore --recurse-submodules a/sm &&
test_path_is_dir a/target/.git &&
test_path_is_missing .git/modules/a/sm &&
test_path_is_missing a/target/submodule_file
'
test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '
prepare_symlink_to_repo &&
rm -rf .git/modules &&
test_must_fail git checkout -f --recurse-submodules initial &&
test_path_is_dir a/target/.git &&
test_path_is_missing .git/modules/a/sm
'
test_done

View file

@ -320,7 +320,7 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '
fi
'
test_expect_success 'git dirs of sibling submodules must not be nested' '
test_expect_success 'setup submodules with nested git dirs' '
git init nested &&
test_commit -C nested nested &&
(
@ -338,9 +338,39 @@ test_expect_success 'git dirs of sibling submodules must not be nested' '
git add .gitmodules thing1 thing2 &&
test_tick &&
git commit -m nested
) &&
)
'
test_expect_success 'git dirs of sibling submodules must not be nested' '
test_must_fail git clone --recurse-submodules nested clone 2>err &&
test_grep "is inside git dir" err
'
test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '
test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&
cat err &&
grep -E "(already exists|is inside git dir|not a git repository)" err &&
{
test_path_is_missing .git/modules/hippo/HEAD ||
test_path_is_missing .git/modules/hippo/hooks/HEAD
}
'
test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' '
git clone nested nested_checkout &&
(
cd nested_checkout &&
git submodule init &&
git submodule update thing1 &&
mkdir -p .git/modules/hippo/hooks/refs &&
mkdir -p .git/modules/hippo/hooks/objects/info &&
echo "../../../../objects" >.git/modules/hippo/hooks/objects/info/alternates &&
echo "ref: refs/heads/master" >.git/modules/hippo/hooks/HEAD
) &&
test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&
cat err &&
grep "is inside git dir" err &&
test_path_is_missing nested_checkout/thing2/.git
'
test_done