Merge branch 'jk/symlinked-dotgitx-cleanup'

Various test and documentation updates about .gitsomething paths
that are symlinks.

* jk/symlinked-dotgitx-cleanup:
  docs: document symlink restrictions for dot-files
  fsck: warn about symlinked dotfiles we'll open with O_NOFOLLOW
  t0060: test ntfs/hfs-obscured dotfiles
  t7450: test .gitmodules symlink matching against obscured names
  t7450: test verify_path() handling of gitmodules
  t7415: rename to expand scope
  fsck_tree(): wrap some long lines
  fsck_tree(): fix shadowed variable
  t7415: remove out-dated comment about translation
This commit is contained in:
Junio C Hamano 2021-05-11 15:27:23 +09:00
commit 416449eaba
13 changed files with 253 additions and 59 deletions

View file

@ -1247,6 +1247,12 @@ to:
[attr]binary -diff -merge -text
------------
NOTES
-----
Git does not follow symbolic links when accessing a `.gitattributes`
file in the working tree. This keeps behavior consistent when the file
is accessed from the index or a tree versus from the filesystem.
EXAMPLES
--------

View file

@ -149,6 +149,10 @@ not tracked by Git remain untracked.
To stop tracking a file that is currently tracked, use
'git rm --cached'.
Git does not follow symbolic links when accessing a `.gitignore` file in
the working tree. This keeps behavior consistent when the file is
accessed from the index or a tree versus from the filesystem.
EXAMPLES
--------

View file

@ -55,6 +55,13 @@ this would also match the 'Commit Name <commit&#64;email.xx>' above:
Proper Name <proper@email.xx> CoMmIt NaMe <CoMmIt@EmAiL.xX>
--
NOTES
-----
Git does not follow symbolic links when accessing a `.mailmap` file in
the working tree. This keeps behavior consistent when the file is
accessed from the index or a tree versus from the filesystem.
EXAMPLES
--------

View file

@ -98,6 +98,14 @@ submodule.<name>.shallow::
shallow clone (with a history depth of 1) unless the user explicitly
asks for a non-shallow clone.
NOTES
-----
Git does not allow the `.gitmodules` file within a working tree to be a
symbolic link, and will refuse to check out such a tree entry. This
keeps behavior consistent when the file is accessed from the index or a
tree versus from the filesystem, and helps Git reliably enforce security
checks of the file contents.
EXAMPLES
--------

View file

@ -1272,6 +1272,7 @@ int is_ntfs_dotgit(const char *name);
int is_ntfs_dotgitmodules(const char *name);
int is_ntfs_dotgitignore(const char *name);
int is_ntfs_dotgitattributes(const char *name);
int is_ntfs_dotmailmap(const char *name);
/*
* Returns true iff "str" could be confused as a command-line option when

84
fsck.c
View file

@ -558,7 +558,7 @@ static int verify_ordered(unsigned mode1, const char *name1,
return c1 < c2 ? 0 : TREE_UNORDERED;
}
static int fsck_tree(const struct object_id *oid,
static int fsck_tree(const struct object_id *tree_oid,
const char *buffer, unsigned long size,
struct fsck_options *options)
{
@ -579,7 +579,9 @@ static int fsck_tree(const struct object_id *oid,
struct name_stack df_dup_candidates = { NULL };
if (init_tree_desc_gently(&desc, buffer, size)) {
retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_BAD_TREE,
"cannot be parsed as a tree");
return retval;
}
@ -589,11 +591,11 @@ static int fsck_tree(const struct object_id *oid,
while (desc.size) {
unsigned short mode;
const char *name, *backslash;
const struct object_id *oid;
const struct object_id *entry_oid;
oid = tree_entry_extract(&desc, &name, &mode);
entry_oid = tree_entry_extract(&desc, &name, &mode);
has_null_sha1 |= is_null_oid(oid);
has_null_sha1 |= is_null_oid(entry_oid);
has_full_path |= !!strchr(name, '/');
has_empty_name |= !*name;
has_dot |= !strcmp(name, ".");
@ -603,23 +605,43 @@ static int fsck_tree(const struct object_id *oid,
if (is_hfs_dotgitmodules(name) || is_ntfs_dotgitmodules(name)) {
if (!S_ISLNK(mode))
oidset_insert(&options->gitmodules_found, oid);
oidset_insert(&options->gitmodules_found,
entry_oid);
else
retval += report(options,
oid, OBJ_TREE,
tree_oid, OBJ_TREE,
FSCK_MSG_GITMODULES_SYMLINK,
".gitmodules is a symbolic link");
}
if (S_ISLNK(mode)) {
if (is_hfs_dotgitignore(name) ||
is_ntfs_dotgitignore(name))
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_GITIGNORE_SYMLINK,
".gitignore is a symlink");
if (is_hfs_dotgitattributes(name) ||
is_ntfs_dotgitattributes(name))
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_GITATTRIBUTES_SYMLINK,
".gitattributes is a symlink");
if (is_hfs_dotmailmap(name) ||
is_ntfs_dotmailmap(name))
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_MAILMAP_SYMLINK,
".mailmap is a symlink");
}
if ((backslash = strchr(name, '\\'))) {
while (backslash) {
backslash++;
has_dotgit |= is_ntfs_dotgit(backslash);
if (is_ntfs_dotgitmodules(backslash)) {
if (!S_ISLNK(mode))
oidset_insert(&options->gitmodules_found, oid);
oidset_insert(&options->gitmodules_found,
entry_oid);
else
retval += report(options, oid, OBJ_TREE,
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_GITMODULES_SYMLINK,
".gitmodules is a symbolic link");
}
@ -628,7 +650,9 @@ static int fsck_tree(const struct object_id *oid,
}
if (update_tree_entry_gently(&desc)) {
retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_BAD_TREE,
"cannot be parsed as a tree");
break;
}
@ -676,25 +700,45 @@ static int fsck_tree(const struct object_id *oid,
name_stack_clear(&df_dup_candidates);
if (has_null_sha1)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_NULL_SHA1, "contains entries pointing to null sha1");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_NULL_SHA1,
"contains entries pointing to null sha1");
if (has_full_path)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_FULL_PATHNAME, "contains full pathnames");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_FULL_PATHNAME,
"contains full pathnames");
if (has_empty_name)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_EMPTY_NAME, "contains empty pathname");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_EMPTY_NAME,
"contains empty pathname");
if (has_dot)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOT, "contains '.'");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_HAS_DOT,
"contains '.'");
if (has_dotdot)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOTDOT, "contains '..'");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_HAS_DOTDOT,
"contains '..'");
if (has_dotgit)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_HAS_DOTGIT, "contains '.git'");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_HAS_DOTGIT,
"contains '.git'");
if (has_zero_pad)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_ZERO_PADDED_FILEMODE, "contains zero-padded file modes");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_ZERO_PADDED_FILEMODE,
"contains zero-padded file modes");
if (has_bad_modes)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_BAD_FILEMODE, "contains bad file modes");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_BAD_FILEMODE,
"contains bad file modes");
if (has_dup_entries)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_DUPLICATE_ENTRIES, "contains duplicate file entries");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_DUPLICATE_ENTRIES,
"contains duplicate file entries");
if (not_properly_sorted)
retval += report(options, oid, OBJ_TREE, FSCK_MSG_TREE_NOT_SORTED, "not properly sorted");
retval += report(options, tree_oid, OBJ_TREE,
FSCK_MSG_TREE_NOT_SORTED,
"not properly sorted");
return retval;
}

3
fsck.h
View file

@ -67,6 +67,9 @@ enum fsck_msg_type {
FUNC(NUL_IN_COMMIT, WARN) \
/* infos (reported as warnings, but ignored by default) */ \
FUNC(GITMODULES_PARSE, INFO) \
FUNC(GITIGNORE_SYMLINK, INFO) \
FUNC(GITATTRIBUTES_SYMLINK, INFO) \
FUNC(MAILMAP_SYMLINK, INFO) \
FUNC(BAD_TAG_NAME, INFO) \
FUNC(MISSING_TAGGER_ENTRY, INFO) \
/* ignored (elevated when requested) */ \

5
path.c
View file

@ -1493,6 +1493,11 @@ int is_ntfs_dotgitattributes(const char *name)
return is_ntfs_dot_str(name, "gitattributes", "gi7d29");
}
int is_ntfs_dotmailmap(const char *name)
{
return is_ntfs_dot_str(name, "mailmap", "maba30");
}
int looks_like_command_line_option(const char *str)
{
return str && str[0] == '-';

View file

@ -172,9 +172,22 @@ static struct test_data dirname_data[] = {
{ NULL, NULL }
};
static int is_dotgitmodules(const char *path)
static int check_dotfile(const char *x, const char **argv,
int (*is_hfs)(const char *),
int (*is_ntfs)(const char *))
{
return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
int res = 0, expect = 1;
for (; *argv; argv++) {
if (!strcmp("--not", *argv))
expect = !expect;
else if (expect != (is_hfs(*argv) || is_ntfs(*argv)))
res = error("'%s' is %s.git%s", *argv,
expect ? "not " : "", x);
else
fprintf(stderr, "ok: '%s' is %s.git%s\n",
*argv, expect ? "" : "not ", x);
}
return !!res;
}
static int cmp_by_st_size(const void *a, const void *b)
@ -382,17 +395,24 @@ int cmd__path_utils(int argc, const char **argv)
return test_function(dirname_data, posix_dirname, argv[1]);
if (argc > 2 && !strcmp(argv[1], "is_dotgitmodules")) {
int res = 0, expect = 1, i;
for (i = 2; i < argc; i++)
if (!strcmp("--not", argv[i]))
expect = !expect;
else if (expect != is_dotgitmodules(argv[i]))
res = error("'%s' is %s.gitmodules", argv[i],
expect ? "not " : "");
else
fprintf(stderr, "ok: '%s' is %s.gitmodules\n",
argv[i], expect ? "" : "not ");
return !!res;
return check_dotfile("modules", argv + 2,
is_hfs_dotgitmodules,
is_ntfs_dotgitmodules);
}
if (argc > 2 && !strcmp(argv[1], "is_dotgitignore")) {
return check_dotfile("ignore", argv + 2,
is_hfs_dotgitignore,
is_ntfs_dotgitignore);
}
if (argc > 2 && !strcmp(argv[1], "is_dotgitattributes")) {
return check_dotfile("attributes", argv + 2,
is_hfs_dotgitattributes,
is_ntfs_dotgitattributes);
}
if (argc > 2 && !strcmp(argv[1], "is_dotmailmap")) {
return check_dotfile("mailmap", argv + 2,
is_hfs_dotmailmap,
is_ntfs_dotmailmap);
}
if (argc > 2 && !strcmp(argv[1], "file-size")) {

View file

@ -468,6 +468,36 @@ test_expect_success 'match .gitmodules' '
.gitmodules,:\$DATA
'
test_expect_success 'match .gitattributes' '
test-tool path-utils is_dotgitattributes \
.gitattributes \
.git${u200c}attributes \
.Gitattributes \
.gitattributeS \
GITATT~1 \
GI7D29~1
'
test_expect_success 'match .gitignore' '
test-tool path-utils is_dotgitignore \
.gitignore \
.git${u200c}ignore \
.Gitignore \
.gitignorE \
GITIGN~1 \
GI250A~1
'
test_expect_success 'match .mailmap' '
test-tool path-utils is_dotmailmap \
.mailmap \
.mail${u200c}map \
.Mailmap \
.mailmaP \
MAILMA~1 \
MABA30~1
'
test_expect_success MINGW 'is_valid_path() on Windows' '
test-tool path-utils is_valid_path \
win32 \

View file

@ -1,9 +1,16 @@
#!/bin/sh
test_description='check handling of .. in submodule names
test_description='check broken or malicious patterns in .git* files
Exercise the name-checking function on a variety of names, and then give a
real-world setup that confirms we catch this in practice.
Such as:
- presence of .. in submodule names;
Exercise the name-checking function on a variety of names, and then give a
real-world setup that confirms we catch this in practice.
- nested submodule names
- symlinked .gitmodules, etc
'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-pack.sh
@ -132,31 +139,84 @@ test_expect_success 'index-pack --strict works for non-repo pack' '
grep gitmodulesName output
'
test_expect_success 'fsck detects symlinked .gitmodules file' '
git init symlink &&
(
cd symlink &&
check_dotx_symlink () {
fsck_must_fail=test_must_fail
fsck_prefix=error
refuse_index=t
case "$1" in
--warning)
fsck_must_fail=
fsck_prefix=warning
refuse_index=
shift
;;
esac
# Make the tree directly to avoid index restrictions.
#
# Because symlinks store the target as a blob, choose
# a pathname that could be parsed as a .gitmodules file
# to trick naive non-symlink-aware checking.
tricky="[foo]bar=true" &&
content=$(git hash-object -w ../.gitmodules) &&
target=$(printf "$tricky" | git hash-object -w --stdin) &&
{
printf "100644 blob $content\t$tricky\n" &&
printf "120000 blob $target\t.gitmodules\n"
} | git mktree &&
name=$1
type=$2
path=$3
dir=symlink-$name-$type
# Check not only that we fail, but that it is due to the
# symlink detector; this grep string comes from the config
# variable name and will not be translated.
test_must_fail git fsck 2>output &&
test_i18ngrep gitmodulesSymlink output
)
'
test_expect_success "set up repo with symlinked $name ($type)" '
git init $dir &&
(
cd $dir &&
# Make the tree directly to avoid index restrictions.
#
# Because symlinks store the target as a blob, choose
# a pathname that could be parsed as a .gitmodules file
# to trick naive non-symlink-aware checking.
tricky="[foo]bar=true" &&
content=$(git hash-object -w ../.gitmodules) &&
target=$(printf "$tricky" | git hash-object -w --stdin) &&
{
printf "100644 blob $content\t$tricky\n" &&
printf "120000 blob $target\t$path\n"
} >bad-tree
) &&
tree=$(git -C $dir mktree <$dir/bad-tree)
'
test_expect_success "fsck detects symlinked $name ($type)" '
(
cd $dir &&
# Check not only that we fail, but that it is due to the
# symlink detector
$fsck_must_fail git fsck 2>output &&
grep "$fsck_prefix.*tree $tree: ${name}Symlink" output
)
'
test -n "$refuse_index" &&
test_expect_success "refuse to load symlinked $name into index ($type)" '
test_must_fail \
git -C $dir \
-c core.protectntfs \
-c core.protecthfs \
read-tree $tree 2>err &&
grep "invalid path.*$name" err &&
git -C $dir ls-files -s >out &&
test_must_be_empty out
'
}
check_dotx_symlink gitmodules vanilla .gitmodules
check_dotx_symlink gitmodules ntfs ".gitmodules ."
check_dotx_symlink gitmodules hfs ".${u200c}gitmodules"
check_dotx_symlink --warning gitattributes vanilla .gitattributes
check_dotx_symlink --warning gitattributes ntfs ".gitattributes ."
check_dotx_symlink --warning gitattributes hfs ".${u200c}gitattributes"
check_dotx_symlink --warning gitignore vanilla .gitignore
check_dotx_symlink --warning gitignore ntfs ".gitignore ."
check_dotx_symlink --warning gitignore hfs ".${u200c}gitignore"
check_dotx_symlink --warning mailmap vanilla .mailmap
check_dotx_symlink --warning mailmap ntfs ".mailmap ."
check_dotx_symlink --warning mailmap hfs ".${u200c}mailmap"
test_expect_success 'fsck detects non-blob .gitmodules' '
git init non-blob &&

5
utf8.c
View file

@ -777,6 +777,11 @@ int is_hfs_dotgitattributes(const char *path)
return is_hfs_dot_str(path, "gitattributes");
}
int is_hfs_dotmailmap(const char *path)
{
return is_hfs_dot_str(path, "mailmap");
}
const char utf8_bom[] = "\357\273\277";
int skip_utf8_bom(char **text, size_t len)

1
utf8.h
View file

@ -61,6 +61,7 @@ int is_hfs_dotgit(const char *path);
int is_hfs_dotgitmodules(const char *path);
int is_hfs_dotgitignore(const char *path);
int is_hfs_dotgitattributes(const char *path);
int is_hfs_dotmailmap(const char *path);
typedef enum {
ALIGN_LEFT,