From: Jeff King <hidden> Date: 2016-11-02 13:05:00
I noticed in a nearby discussion that we will follow in-filesystem
symlinks for in-tree .gitignore and .gitattributes files, but not when
those files are read out of the index (e.g., when switching branches).
This series teaches git to open those files with O_NOFOLLOW (when it is
available) to get more consistent behavior. Note that this only applies
to the in-tree versions; you can still symlink $GIT_DIR/info/attributes,
etc.
I stopped short of warning about symlinked entries in git-fsck, but
perhaps we would want to do that as well (doing it completely is tricky
because of all of the case-folding issues around matching pathnames).
[1/5]: add open_nofollow() helper
[2/5]: attr: convert "macro_ok" into a flags field
[3/5]: exclude: convert "check_index" into a flags field
[4/5]: attr: do not respect symlinks for in-tree .gitattributes
[5/5]: exclude: do not respect symlinks for in-tree .gitignore
attr.c | 58 ++++++++++++++++++++++++++++++++-------------------
dir.c | 20 +++++++++++++-----
dir.h | 2 +-
git-compat-util.h | 3 +++
t/t0003-attributes.sh | 31 +++++++++++++++++++++++++++
t/t0008-ignores.sh | 29 ++++++++++++++++++++++++++
wrapper.c | 8 +++++++
7 files changed, 123 insertions(+), 28 deletions(-)
-Peff
From: Jeff King <hidden> Date: 2016-11-02 13:06:37
Some callers of open() would like to optionally use
O_NOFOLLOW, but it is not available on all platforms. We
could abstract this by publicly defining O_NOFOLLOW to 0 on
those platforms, but that leaves us no room for any
workarounds (e.g., by checking the file type via lstat()).
Instead, let's abstract it into its own function. We don't
implement any workarounds here, but it it would be easy to
add them later.
Signed-off-by: Jeff King <redacted>
---
I didn't add the workaround because I think the current callers are OK
with "best effort", and doing it non-racily is quite tricky (though we
might also be OK with a racy version; we are not trying to beat
/tmp races, but just making sure a checkout that we did is sane).
git-compat-util.h | 3 +++
wrapper.c | 8 ++++++++
2 files changed, 11 insertions(+)
@@ -1080,6 +1080,9 @@ int access_or_die(const char *path, int mode, unsigned flag);/* Warn on an inaccessible file that ought to be accessible */voidwarn_on_inaccessible(constchar*path);+/* Open with O_NOFOLLOW, if available on this platform */+intopen_nofollow(constchar*path,intflags);+#ifdef GMTIME_UNRELIABLE_ERRORSstructtm*git_gmtime(consttime_t*);structtm*git_gmtime_r(consttime_t*,structtm*);
From: Jeff King <hidden> Date: 2016-11-02 13:06:52
The attribute code can have a rather deep callstack, through
which we have to pass the "macro_ok" flag. In anticipation
of adding other flags, let's convert this to a generic
bit-field.
Signed-off-by: Jeff King <redacted>
---
attr.c | 43 ++++++++++++++++++++++++-------------------
1 file changed, 24 insertions(+), 19 deletions(-)
@@ -151,6 +151,9 @@ struct match_attr {staticconstcharblank[]=" \t\r\n";+/* Flags usable in read_attr() and parse_attr_line() family of functions. */+#define READ_ATTR_MACRO_OK (1<<0)+/**Parseawhitespace-delimitedattributestate(i.e.,"attr",*"-attr","!attr",or"attr=value")fromthestringstartingatsrc.
From: Jeff King <hidden> Date: 2016-11-02 13:07:16
We pass the "check_index" flag through the variants of
add_excludes(). Let's turn this into a full flags bit-field,
so that we can add more flags to it without affecting the
function signature.
Note that only one caller actually needs to use the new flag
name, as the rest all were passing "0" already.
Signed-off-by: Jeff King <redacted>
---
dir.c | 13 +++++++++----
dir.h | 2 +-
2 files changed, 10 insertions(+), 5 deletions(-)
From: Jeff King <hidden> Date: 2016-11-02 13:08:55
The attributes system may sometimes read in-tree files from
the filesystem, and sometimes from the index. In the latter
case, we do not resolve symbolic links (and are not likely
to ever start doing so). Let's open filesystem links with
O_NOFOLLOW so that the two cases behave consistently.
As a bonus, this means that git will not follow such
symlinks to read and parse out-of-tree paths. It's unlikely
that this could have any security implications (a malicious
repository can already feed arbitrary content to the
attribute parser, and any disclosure of the out-of-tree
contents happens only to stderr). But it's one less oddball
thing to worry about.
Note that O_NOFOLLOW only prevents following links for the
path itself, not intermediate directories in the path. At
first glance, it seems like
ln -s /some/path in-repo
might still look at "in-repo/.gitattributes", following the
symlink to "/some/path/.gitattributes". However, if
"in-repo" is a symbolic link, then we know that it has no
git paths below it, and will never look at its
.gitattributes file.
We will continue to support out-of-tree symbolic links
(e.g., in $GIT_DIR/info/attributes); this just affects
in-tree links. When a symbolic link is encountered, the
contents are ignored and a warning is printed. POSIX
specifies ELOOP in this case, so the user would generally
see something like:
warning: unable to access '.gitattributes': Too many levels of symbolic links
Signed-off-by: Jeff King <redacted>
---
attr.c | 17 +++++++++++++----
t/t0003-attributes.sh | 31 +++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 4 deletions(-)
On Wed, Nov 2, 2016 at 8:08 PM, Jeff King [off-list ref] wrote:
The attributes system may sometimes read in-tree files from
the filesystem, and sometimes from the index. In the latter
case, we do not resolve symbolic links (and are not likely
to ever start doing so). Let's open filesystem links with
O_NOFOLLOW so that the two cases behave consistently.
This sounds backward to me. The major use case is reading
.gitattributes on worktree, which follows symlinks so far. Only
git-archive has a special need to read index-only versions. The
worktree behavior should influence the in-index one, not the other way
around. If we could die("BUG" when git-archive is used on symlinks
(without --worktree-attributes). If people are annoyed by it, they can
implement symlink folllowing (to another version in index, not on
worktree).
The story is similar for .gitignore where in-index version is merely
an optimization. If it's symlinks and we can't follow, we should fall
back to worktree version.
--
Duy
From: Jeff King <hidden> Date: 2016-11-07 21:10:16
On Mon, Nov 07, 2016 at 05:03:42PM +0700, Duy Nguyen wrote:
On Wed, Nov 2, 2016 at 8:08 PM, Jeff King [off-list ref] wrote:
quoted
The attributes system may sometimes read in-tree files from
the filesystem, and sometimes from the index. In the latter
case, we do not resolve symbolic links (and are not likely
to ever start doing so). Let's open filesystem links with
O_NOFOLLOW so that the two cases behave consistently.
This sounds backward to me. The major use case is reading
.gitattributes on worktree, which follows symlinks so far. Only
git-archive has a special need to read index-only versions. The
worktree behavior should influence the in-index one, not the other way
around. If we could die("BUG" when git-archive is used on symlinks
(without --worktree-attributes). If people are annoyed by it, they can
implement symlink folllowing (to another version in index, not on
worktree).
I agree it feels a little backwards, as we are choosing the
lowest-common denominator of the two (so it would be reasonable to have
the in-index version follow symbolic links, or at least do so on
platforms where core.symlinks is true).
And I'll admit my main motivation is not that index/filesystem parity,
but rather just that:
git clone git://host.com/malicious-repo.git
git log
might create and read symlinks to arbitrary files on the cloner's box.
I'm not sure to what degree to be worried about that. It's not like you
can't make other arbitrary symlinks which are likely to be read if the
user actually starts looking at checked-out files. It's just that we
usually try to make a clone+log of a malicious repository safe.
That being said, I'm not convinced that reading the index version of
.gitattributes and .gitignore is just an optimization. Don't we read the
destination attributes when checking out a new tree? And doesn't merge
need to use the in-index version when we see conflicts?
So I was hoping that this was a practice that is unlikely to be in wide
use, and that we could simply ban in order to keep the attribute and
ignore code simpler and safer, both now and if we change them to do more
in-index lookups.
-Peff
From: Jeff King <hidden> Date: 2016-11-07 21:15:29
On Mon, Nov 07, 2016 at 04:10:10PM -0500, Jeff King wrote:
And I'll admit my main motivation is not that index/filesystem parity,
but rather just that:
git clone git://host.com/malicious-repo.git
git log
might create and read symlinks to arbitrary files on the cloner's box.
I'm not sure to what degree to be worried about that. It's not like you
can't make other arbitrary symlinks which are likely to be read if the
user actually starts looking at checked-out files. It's just that we
usually try to make a clone+log of a malicious repository safe.
Another approach is to have a config option to disallow symlinks to
destinations outside of the repository tree (I'm not sure if it should
be on or off by default, though).
Again, I don't know that there is a specific security issue, but it
makes things easier for services which might clone untrusted
repositories (e.g., things like CI). They'd obviously have to be careful
with the contents of the repositories anyway, but it's one less thing to
have to worry about.
-Peff
On Tue, Nov 8, 2016 at 4:15 AM, Jeff King [off-list ref] wrote:
On Mon, Nov 07, 2016 at 04:10:10PM -0500, Jeff King wrote:
quoted
And I'll admit my main motivation is not that index/filesystem parity,
but rather just that:
git clone git://host.com/malicious-repo.git
git log
might create and read symlinks to arbitrary files on the cloner's box.
I'm not sure to what degree to be worried about that. It's not like you
can't make other arbitrary symlinks which are likely to be read if the
user actually starts looking at checked-out files. It's just that we
usually try to make a clone+log of a malicious repository safe.
This I can buy.
Another approach is to have a config option to disallow symlinks to
destinations outside of the repository tree (I'm not sure if it should
be on or off by default, though).
Let's err on the safe side and disable symlinks to outside repo by
default (or even all symlinks on .gitattributes and .gitignore as the
first step)
What I learned from my changes in .gitignore is, if we have not
forbidden something, people likely find some creative use for it. As
long as it's can be turned on or off, i guess those minority will stay
happy.
Again, I don't know that there is a specific security issue, but it
makes things easier for services which might clone untrusted
repositories (e.g., things like CI). They'd obviously have to be careful
with the contents of the repositories anyway, but it's one less thing to
have to worry about.
-Peff
From: Jeff King <hidden> Date: 2016-11-08 22:21:35
On Tue, Nov 08, 2016 at 08:38:55AM +0700, Duy Nguyen wrote:
quoted
Another approach is to have a config option to disallow symlinks to
destinations outside of the repository tree (I'm not sure if it should
be on or off by default, though).
Let's err on the safe side and disable symlinks to outside repo by
default (or even all symlinks on .gitattributes and .gitignore as the
first step)
Both of those are actually much harder than you might think.
For matching specific names, we have to deal with case-folding. It's
easy to hit the common ones like ".GITIGNORE" with fspathcmp(). But if
this is actually protection against malicious repositories, we have to
match all of the horrible filesystem-specific junk that we did for
".git".
Symlinks are likewise tricky. If we see that a symlink points to
"foo/../bar", then we don't know if it leaves the repository unless we
also look at "foo" to see if it is also a symlink. So you really end up
having to resolve the symlink yourself (and when checking out multiple
files, there's an ordering dependency).
I think it might be enough to check:
- leading "../" tokens in the symlink's destination can be checked
against the symlink's path. So "../foo" is OK for path "one/two",
but not for path "one".
- interior "../" can be disallowed entirely. Technically
"foo/../bar/../baz" _can_ be a fine symlink destination, but why?
It's identical to "baz" unless you are following a bunch of interior
symlinks. And if those are interior symlinks, it's still confusing
and unnecessarily obfuscated, and a good sign that somebody is
trying to do something tricky.
So one reasonable fix might be to have a config option like
"core.saneSymlinks" that enforces both of those rules for _all_ symlinks
that we checkout to the working tree. And it could either refuse to
check them out, or replace them with a file containing the symlink
content (as we do on systems that don't support symlinks, IIRC).
It could even be off by default (for backwards compatibility, as there
really are uses for symlinks reaching out of the repository in some
cases), but people cloning untrusted repos could flip it on. That seems
like an improvement over the current state.
What I learned from my changes in .gitignore is, if we have not
forbidden something, people likely find some creative use for it. As
long as it's can be turned on or off, i guess those minority will stay
happy.
Yes, it's one of the fun things about working on a 10-year-old project.
:)
-Peff
On Wed, Nov 9, 2016 at 5:21 AM, Jeff King [off-list ref] wrote:
On Tue, Nov 08, 2016 at 08:38:55AM +0700, Duy Nguyen wrote:
quoted
quoted
Another approach is to have a config option to disallow symlinks to
destinations outside of the repository tree (I'm not sure if it should
be on or off by default, though).
Let's err on the safe side and disable symlinks to outside repo by
default (or even all symlinks on .gitattributes and .gitignore as the
first step)
Both of those are actually much harder than you might think.
For matching specific names, we have to deal with case-folding. It's
easy to hit the common ones like ".GITIGNORE" with fspathcmp(). But if
this is actually protection against malicious repositories, we have to
match all of the horrible filesystem-specific junk that we did for
".git".
We could realpath() it and check if the result path is inside
realpath($GIT_WORK_TREE). The real work would be done by OS. We will
need to check if it points to .git/something, but I think we have that
covered. The approach is a bit heavy for such a sanity check though
Symlinks are likewise tricky. If we see that a symlink points to
"foo/../bar", then we don't know if it leaves the repository unless we
also look at "foo" to see if it is also a symlink. So you really end up
having to resolve the symlink yourself (and when checking out multiple
files, there's an ordering dependency).
We do have this dependency problem right now (e.g. files A and
.gitattributes are checked out at the same time and .gitattributes has
some attribute on A). It looks like we resolve it by reading the index
version at checkout time. We probably can do the same for gitattribute
symlinks.
I think it might be enough to check:
- leading "../" tokens in the symlink's destination can be checked
against the symlink's path. So "../foo" is OK for path "one/two",
but not for path "one".
- interior "../" can be disallowed entirely. Technically
"foo/../bar/../baz" _can_ be a fine symlink destination, but why?
It's identical to "baz" unless you are following a bunch of interior
symlinks. And if those are interior symlinks, it's still confusing
and unnecessarily obfuscated, and a good sign that somebody is
trying to do something tricky.
Sounds good.
So one reasonable fix might be to have a config option like
"core.saneSymlinks" that enforces both of those rules for _all_ symlinks
that we checkout to the working tree. And it could either refuse to
check them out, or replace them with a file containing the symlink
content (as we do on systems that don't support symlinks, IIRC).
I wonder if anyone want core.saneSymlinks on, but they have some links
that do not meet the above checks and still want to follow them
anyway. One way to add such an exception is mark the path with an
attribute "follow". Yeah I have a dependency loop :(
--
Duy
From: Jeff King <hidden> Date: 2016-11-09 16:45:46
On Wed, Nov 09, 2016 at 04:22:12PM +0700, Duy Nguyen wrote:
quoted
Symlinks are likewise tricky. If we see that a symlink points to
"foo/../bar", then we don't know if it leaves the repository unless we
also look at "foo" to see if it is also a symlink. So you really end up
having to resolve the symlink yourself (and when checking out multiple
files, there's an ordering dependency).
We do have this dependency problem right now (e.g. files A and
.gitattributes are checked out at the same time and .gitattributes has
some attribute on A). It looks like we resolve it by reading the index
version at checkout time. We probably can do the same for gitattribute
symlinks.
Right, but then we can't use filesystem functions like realpath() to do
the lookup. I guess we could do a pass after the checkout is done to
"fix" any out-of-tree symlinks we just created.
This is exactly the sort of complexity I was trying to avoid with my
original series. :)
If that isn't an option, I think I prefer something like the
core.saneSymlinks approach I mentioned. It has the additional bonus of
protecting not just git commands, but other commands that might inspect
the filesystem.
quoted
So one reasonable fix might be to have a config option like
"core.saneSymlinks" that enforces both of those rules for _all_ symlinks
that we checkout to the working tree. And it could either refuse to
check them out, or replace them with a file containing the symlink
content (as we do on systems that don't support symlinks, IIRC).
I wonder if anyone want core.saneSymlinks on, but they have some links
that do not meet the above checks and still want to follow them
anyway. One way to add such an exception is mark the path with an
attribute "follow". Yeah I have a dependency loop :(
That could come later if somebody wants it, I think (especially if the
config option is not on by default). I have a feeling that callers will
either care about out-of-tree symlinks or not. Trusting the repository
to say "but these ones are OK" doesn't work for the paranoid ones, and
everybody else just assumes the repository is sane.
-Peff
From: Junio C Hamano <hidden> Date: 2016-11-09 22:58:53
Duy Nguyen [off-list ref] writes:
Let's err on the safe side and disable symlinks to outside repo by
default (or even all symlinks on .gitattributes and .gitignore as the
first step)
What I learned from my changes in .gitignore is, if we have not
forbidden something, people likely find some creative use for it.
Yup. Supporting any symlink in-tree is like requiring Git to be
used only on symlink-capable filesystems. Not allowing it sounds
like a very sensible option and unlike true contents, there is no
downside to give that limitation to things like .git<anything>.
Shouldn't we do the same for .gitmodules while we are at it?
From: Jeff King <hidden> Date: 2016-11-09 23:17:27
On Wed, Nov 09, 2016 at 02:58:37PM -0800, Junio C Hamano wrote:
Duy Nguyen [off-list ref] writes:
quoted
Let's err on the safe side and disable symlinks to outside repo by
default (or even all symlinks on .gitattributes and .gitignore as the
first step)
What I learned from my changes in .gitignore is, if we have not
forbidden something, people likely find some creative use for it.
Yup. Supporting any symlink in-tree is like requiring Git to be
used only on symlink-capable filesystems. Not allowing it sounds
like a very sensible option and unlike true contents, there is no
downside to give that limitation to things like .git<anything>.
I'm slightly confused. Did you mean "supporting any in-tree symlink to
an out-of-tree destination" in your first sentence?
Shouldn't we do the same for .gitmodules while we are at it?
Good catch. Though I am inclined to have a flag that just covers all
out-of-tree symlinks, regardless of names.
-Peff
From: Jeff King <hidden> Date: 2016-11-02 13:09:17
Like .gitattributes, we would like to make sure that
.gitignore files are handled consistently whether read from
the index or from the filesystem. We can do so by using
O_NOFOLLOW when opening the files.
Signed-off-by: Jeff King <redacted>
---
dir.c | 9 +++++++--
t/t0008-ignores.sh | 29 +++++++++++++++++++++++++++++
2 files changed, 36 insertions(+), 2 deletions(-)
$ git diff --stat origin/master..origin/sb/attr |grep attr.c
attr.c | 531 +-
From a cursory read of your series this may result in a merge
conflict, but would be
easily fixable (changed signature of functions that clash).
$ git diff --stat origin/master..origin/sb/attr |grep attr.c
attr.c | 531 +-
From a cursory read of your series this may result in a merge
conflict, but would be
easily fixable (changed signature of functions that clash).
Yeah, I knew you guys were doing some refactoring of the attribute code
elsewhere, but hadn't actually seen how bad the damage was. I just did
the merge with sb/attr, though, and the conflicts are quite trivial
(mostly s/1/flags/ in bootstrap_attr_stack()).
I'm happy to re-roll on a different base if sb/attr graduates first, but
I suspect Junio can just resolve the conflicts at merge time.
-Peff