From: Junio C Hamano <hidden> Date: 2016-06-15 23:04:47
Jeff King [off-list ref] writes:
The solution is fairly straight-forward: we read the request
body into an in-memory buffer in http-backend, freeing up
Apache, and then feed the data ourselves to upload-pack. But
there are a few important things to note:
1. We limit in-memory buffer to no larger than 1 megabyte
to prevent an obvious denial-of-service attack. This
is a new hard limit on requests, but it's likely that
requests of this size didn't work before at all (i.e.,
they would have run into the pipe buffer thing and
deadlocked).
2. We must take care only to buffer when we have to. For
pushes, the incoming packfile may be of arbitrary
size, and we should connect the input directly to
receive-pack. There's no deadlock problem here, though,
because we do not produce any output until the whole
packfile has been read.
For upload-pack's initial ref advertisement, we
similarly do not need to buffer. Even though we may
generate a lot of output, there is no request body at
all (i.e., it is a GET, not a POST).
Thanks.
One unrelated thing I noticed was that three codepaths independently
have close(0) in run_service() now, and made me follow the two
helper functions to see they both do the close at the end. It might
have made the flow easier to follow if run_service() were
...
close(1);
if (gzip)
inflate();
else if (buffer)
copy();
close(0);
...
But that is minor.
Also, is it worth allocating small and then growing up to the maximum?
I think this only relays one request at a time anyway, and I suspect
that a single 1MB allocation at the first call kept getting reused
may be sufficient (and much simpler).
@@ -266,9 +267,49 @@ static struct rpc_service *select_service(const char *name)returnsvc;}-staticvoidinflate_request(constchar*prog_name,intout)+/*+*Thisisbasicallystrbuf_read(),exceptthatifwe+*hitMAX_REQUEST_BUFFERwedie(we'dratherrejecta+*maliciouslylargerequestthanchewupinfinitememory).+*/+#define MAX_REQUEST_BUFFER (1024 * 1024)+staticssize_tread_request(intfd,unsignedchar**out)+{+size_tlen=0,alloc=8192;+unsignedchar*buf=xmalloc(alloc);++while(1){+ssize_tcnt;++cnt=read_in_full(fd,buf+len,alloc-len);+if(cnt<0){+free(buf);+return-1;+}++/* partial read from read_in_full means we hit EOF */+len+=cnt;+if(len<alloc){+*out=buf;+returnlen;+}++/* otherwise, grow and try again (if we can) */+if(alloc==MAX_REQUEST_BUFFER)+die("request was larger than our maximum size (%lu)",+(unsignedlong)(MAX_REQUEST_BUFFER-1));++alloc=alloc_nr(alloc);+if(alloc>MAX_REQUEST_BUFFER)+alloc=MAX_REQUEST_BUFFER;+REALLOC_ARRAY(buf,alloc);+}+}++staticvoidinflate_request(constchar*prog_name,intout,intbuffer_input){git_zstreamstream;+unsignedchar*full_request=NULL;unsignedcharin_buf[8192];unsignedcharout_buf[8192];unsignedlongcnt=0;
@@ -277,11 +318,21 @@ static void inflate_request(const char *prog_name, int out)git_inflate_init_gzip_only(&stream);while(1){-ssize_tn=xread(0,in_buf,sizeof(in_buf));+ssize_tn;++if(buffer_input){+if(full_request)+n=0;/* nothing left to read */+else+n=read_request(0,&full_request);+stream.next_in=full_request;+}else{+n=xread(0,in_buf,sizeof(in_buf));+stream.next_in=in_buf;+}+if(n<=0)die("request ended in the middle of the gzip stream");--stream.next_in=in_buf;stream.avail_in=n;while(0<stream.avail_in){
From: Konstantin Ryabitsev <hidden> Date: 2016-06-15 23:04:47
On 15/05/15 02:22 PM, Junio C Hamano wrote:
Also, is it worth allocating small and then growing up to the maximum?
I think this only relays one request at a time anyway, and I suspect
that a single 1MB allocation at the first call kept getting reused
may be sufficient (and much simpler).
Does it make sense to make that configurable via an env variable at all?
I suspect the vast majority of people would not hit this bug unless they
are dealing with repositories polluted with hundreds of refs created by
automation (like the codeaurora chromium repo).
E.g. can be set via an Apache directive like
SetEnv FOO_MAX_SIZE 2048
The backend can then be configured to emit an error message when the
spool size is exhausted saying "foo exhausted, increase FOO_MAX_SIZE to
allow for moar foo."
-K
From: Jeff King <hidden> Date: 2016-06-15 23:04:47
On Fri, May 15, 2015 at 11:22:42AM -0700, Junio C Hamano wrote:
Jeff King [off-list ref] writes:
quoted
The solution is fairly straight-forward: we read the request
body into an in-memory buffer in http-backend, freeing up
Apache, and then feed the data ourselves to upload-pack. But
there are a few important things to note:
1. We limit in-memory buffer to no larger than 1 megabyte
to prevent an obvious denial-of-service attack. This
is a new hard limit on requests, but it's likely that
requests of this size didn't work before at all (i.e.,
they would have run into the pipe buffer thing and
deadlocked).
So this 1MB limit is clearly a problem, and the reasoning above is not
right. The case we are helping is when a large amount of input creates a
large amount of output. But we're _hurting_ the case where there's just
a large amount of input (as shown by the Dennis's test case).
What do we want to do about that? We can switch to streaming after
hitting our limit (so opening the opportunity for deadlock again in some
cases, but making sure we do no harm to cases that currently work). Or
we can just bump the input size and say "you'd be crazy to send more
than 10MB" (or 50, or whatever). We could make a configuration knob,
too, I guess.
One unrelated thing I noticed was that three codepaths independently
have close(0) in run_service() now, and made me follow the two
helper functions to see they both do the close at the end. It might
have made the flow easier to follow if run_service() were
...
close(1);
if (gzip)
inflate();
else if (buffer)
copy();
close(0);
...
But that is minor.
I don't see the close(0) in the other (buffered) code paths. We close
the _output_ to the child, but of course we have to do that to tell it
we're done sending (I actually forgot it in an earlier version of
copy_request(), and things hang :) ).
I don't think there's any need to close(0) in the buffered cases. We
read until EOF in the copy() case. For gzip, we read until the end of
the gzipped data. I guess it would be better to close if we're not
expecting more input, as otherwise Apache might block trying to write to
us if the client sends bogus input (i.e., a zlib stream with more cruft
at the end).
Also, is it worth allocating small and then growing up to the maximum?
I think this only relays one request at a time anyway, and I suspect
that a single 1MB allocation at the first call kept getting reused
may be sufficient (and much simpler).
My initial attempt did exactly that, but I had a much smaller buffer. I
started to get worried around 1MB. If we bump it to 10MB (or make it
configurable), I get more so. I dunno. It is not _that_ much memory, but
it is per-request we are serving, so it might add up on a busy server.
OTOH, pack-objects thinks nothing of allocating 800MB just for the
book-keeping to serve a clone of torvalds/linux.
-Peff
From: Jeff King <hidden> Date: 2016-06-15 23:04:50
On Fri, May 15, 2015 at 02:28:37PM -0400, Konstantin Ryabitsev wrote:
On 15/05/15 02:22 PM, Junio C Hamano wrote:
quoted
Also, is it worth allocating small and then growing up to the maximum?
I think this only relays one request at a time anyway, and I suspect
that a single 1MB allocation at the first call kept getting reused
may be sufficient (and much simpler).
Does it make sense to make that configurable via an env variable at all?
I suspect the vast majority of people would not hit this bug unless they
are dealing with repositories polluted with hundreds of refs created by
automation (like the codeaurora chromium repo).
E.g. can be set via an Apache directive like
SetEnv FOO_MAX_SIZE 2048
The backend can then be configured to emit an error message when the
spool size is exhausted saying "foo exhausted, increase FOO_MAX_SIZE to
allow for moar foo."
Yeah, that was the same conclusion I came to elsewhere in the thread.
Here's a re-roll:
[1/3]: http-backend: fix die recursion with custom handler
[2/3]: t5551: factor out tag creation
[3/3]: http-backend: spool ref negotiation requests to buffer
It makes the size configurable (either through the environment, which is
convenient for setting via Apache; or through the config, which is
convenient if you have one absurdly-sized repo). It mentions the
variable name when it barfs into the Apache log. I also bumped the
default to 10MB, which I think should be enough to handle even
ridiculous cases.
I also adapted Dennis's test into the third patch. Beware that it's
quite slow to run (and is protected by the "EXPENSIVE" prerequisite).
Patch 2 is new, and just refactors the script to make adding the new
test easier.
I really wanted to add a test like:
@@ -273,6 +273,16 @@ test_expect_success 'large fetch-pack requests can be split across POSTs' 'test_line_count=2posts'+test_expect_success'http-backend does not buffer arbitrarily large requests''+test_when_finished"(+cd\"$HTTPD_DOCUMENT_ROOT_PATH/repo.git\"&&+test_unconfighttp.maxrequestbuffer+)" &&+git-C"$HTTPD_DOCUMENT_ROOT_PATH/repo.git"\+confighttp.maxrequestbuffer100&&+test_must_failgitclone$HTTPD_URL/smart/repo.gitfoo.git+'+ test_expect_successEXPENSIVE'http can handle enormous ref negotiation''(cd"$HTTPD_DOCUMENT_ROOT_PATH/repo.git"&&
to test that the maxRequestBuffer code does indeed work. Unfortunately,
even though the server behaves reasonably in this case, the client ends
up hanging forever. I'm not sure there is a simple solution to that; I
think it is a protocol issue where remote-http is waiting for fetch-pack
to speak, but fetch-pack is waiting for more data from the remote.
Personally, I think I'd be much more interested in pursuing a saner,
full duplex http solution like git-over-websockets than trying to iron
out all of the corner cases in the stateless-rpc protocol.
-Peff
From: Jeff King <hidden> Date: 2016-06-15 23:04:50
When we die() in http-backend, we call a custom handler that
writes an HTTP 500 response to stdout, then reports the
error to stderr. Our routines for writing out the HTTP
response may themselves die, leading to us entering die()
again.
When it was originally written, that was OK; our custom
handler keeps a variable to notice this and does not
recurse. However, since cd163d4 (usage.c: detect recursion
in die routines and bail out immediately, 2012-11-14), the
main die() implementation detects recursion before we even
get to our custom handler, and bails without printing
anything useful.
We can handle this case by doing two things:
1. Installing a custom die_is_recursing handler that
allows us to enter up to one level of recursion. Only
the first call to our custom handler will try to write
out the error response. So if we die again, that is OK.
If we end up dying more than that, it is a sign that we
are in an infinite recursion.
2. Reporting the error to stderr before trying to write
out the HTTP response. In the current code, if we do
die() trying to write out the response, we'll exit
immediately from this second die(), and never get a
chance to output the original error (which is almost
certainly the more interesting one; the second die is
just going to be along the lines of "I tried to write
to stdout but it was closed").
Signed-off-by: Jeff King <redacted>
---
http-backend.c | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
From: Jeff King <hidden> Date: 2016-06-15 23:04:50
One of our tests in t5551 creates a large number of tags,
and jumps through some hoops to do it efficiently. Let's
factor that out into a function so we can make other similar
tests.
Signed-off-by: Jeff King <redacted>
---
t/t5551-http-fetch-smart.sh | 34 +++++++++++++++++++++-------------
1 file changed, 21 insertions(+), 13 deletions(-)
@@ -224,27 +224,35 @@ test_expect_success 'transfer.hiderefs works over smart-http' 'git-Chidden.gitrev-parse--verifyb'-test_expect_success'create 2,000 tags in the repo''-(-cd"$HTTPD_DOCUMENT_ROOT_PATH/repo.git"&&-foriin$(test_seq2000)+# create an arbitrary number of tags, numbered from tag-$1 to tag-$2+create_tags(){+rm-fmarks&&+foriin$(test_seq"$1""$2")do-echo"commit refs/heads/too-many-refs"-echo"mark :$i"-echo"committer git <git@example.com> $i +0000"-echo"data 0"-echo"M 644 inline bla.txt"-echo"data 4"-echo"bla"+# don't use here-doc, because it requires a process+# per loop iteration+echo"commit refs/heads/too-many-refs-$1"&&+echo"mark :$i"&&+echo"committer git <git@example.com> $i +0000"&&+echo"data 0"&&+echo"M 644 inline bla.txt"&&+echo"data 4"&&+echo"bla"&&# make every commit dangling by always# rewinding the branch after each commit-echo"reset refs/heads/too-many-refs"-echo"from :1"+echo"reset refs/heads/too-many-refs-$1"&&+echo"from :$1"done|gitfast-import--export-marks=marks&&# now assign tags to all the dangling commits we created abovetag=$(perl-e"print \"bla\" x 30")&&sed-e"s|^:\([^ ]*\) \(.*\)$|\2 refs/tags/$tag-\1|"<marks>>packed-refs+}++test_expect_success'create 2,000 tags in the repo''+(+cd"$HTTPD_DOCUMENT_ROOT_PATH/repo.git"&&+create_tags12000)'
From: Jeff King <hidden> Date: 2016-06-15 23:04:50
When http-backend spawns "upload-pack" to do ref
negotiation, it streams the http request body to
upload-pack, who then streams the http response back to the
client as it reads. In theory, git can go full-duplex; the
client can consume our response while it is still sending
the request. In practice, however, HTTP is a half-duplex
protocol. Even if our client is ready to read and write
simultaneously, we may have other HTTP infrastructure in the
way, including the webserver that spawns our CGI, or any
intermediate proxies.
In at least one documented case[1], this leads to deadlock
when trying a fetch over http. What happens is basically:
1. Apache proxies the request to the CGI, http-backend.
2. http-backend gzip-inflates the data and sends
the result to upload-pack.
3. upload-pack acts on the data and generates output over
the pipe back to Apache. Apache isn't reading because
it's busy writing (step 1).
This works fine most of the time, because the upload-pack
output ends up in a system pipe buffer, and Apache reads
it as soon as it finishes writing. But if both the request
and the response exceed the system pipe buffer size, then we
deadlock (Apache blocks writing to http-backend,
http-backend blocks writing to upload-pack, and upload-pack
blocks writing to Apache).
We need to break the deadlock by spooling either the input
or the output. In this case, it's ideal to spool the input,
because Apache does not start reading either stdout _or_
stderr until we have consumed all of the input. So until we
do so, we cannot even get an error message out to the
client.
The solution is fairly straight-forward: we read the request
body into an in-memory buffer in http-backend, freeing up
Apache, and then feed the data ourselves to upload-pack. But
there are a few important things to note:
1. We limit the in-memory buffer to prevent an obvious
denial-of-service attack. This is a new hard limit on
requests, but it's unlikely to come into play. The
default value is 10MB, which covers even the ridiculous
100,000-ref negotation in the included test (that
actually caps out just over 5MB). But it's configurable
on the off chance that you don't mind spending some
extra memory to make even ridiculous requests work.
2. We must take care only to buffer when we have to. For
pushes, the incoming packfile may be of arbitrary
size, and we should connect the input directly to
receive-pack. There's no deadlock problem here, though,
because we do not produce any output until the whole
packfile has been read.
For upload-pack's initial ref advertisement, we
similarly do not need to buffer. Even though we may
generate a lot of output, there is no request body at
all (i.e., it is a GET, not a POST).
[1] http://article.gmane.org/gmane.comp.version-control.git/269020
Test-adapted-from: Dennis Kaarsemaker [off-list ref]
Signed-off-by: Jeff King <redacted>
---
Documentation/git-http-backend.txt | 9 ++++
http-backend.c | 97 +++++++++++++++++++++++++++++++++-----
t/t5551-http-fetch-smart.sh | 15 ++++++
3 files changed, 110 insertions(+), 11 deletions(-)
@@ -255,6 +255,15 @@ The GIT_HTTP_EXPORT_ALL environmental variable may be passed to 'git-http-backend' to bypass the check for the "git-daemon-export-ok" file in each repository before allowing export of that repository.+The `GIT_HTTP_MAX_REQUEST_BUFFER` environment variable (or the+`http.maxRequestBuffer` config variable) may be set to change the+largest ref negotiation request that git will handle during a fetch; any+fetch requiring a larger buffer will not succeed. This value should not+normally need to be changed, but may be helpful if you are fetching from+a repository with an extremely large number of refs. The value can be+specified with a unit (e.g., `100M` for 100 megabytes). The default is+10 megabytes.+ The backend process sets GIT_COMMITTER_NAME to '$REMOTE_USER' and GIT_COMMITTER_EMAIL to '$\{REMOTE_USER}@http.$\{REMOTE_ADDR\}', ensuring that any reflogs created by 'git-receive-pack' contain some
@@ -266,9 +269,53 @@ static struct rpc_service *select_service(const char *name)returnsvc;}-staticvoidinflate_request(constchar*prog_name,intout)+/*+*Thisisbasicallystrbuf_read(),exceptthatifwe+*hitmax_request_bufferwedie(we'dratherrejecta+*maliciouslylargerequestthanchewupinfinitememory).+*/+staticssize_tread_request(intfd,unsignedchar**out)+{+size_tlen=0,alloc=8192;+unsignedchar*buf=xmalloc(alloc);++if(max_request_buffer<alloc)+max_request_buffer=alloc;++while(1){+ssize_tcnt;++cnt=read_in_full(fd,buf+len,alloc-len);+if(cnt<0){+free(buf);+return-1;+}++/* partial read from read_in_full means we hit EOF */+len+=cnt;+if(len<alloc){+*out=buf;+warning("request size was %lu",(unsignedlong)len);+returnlen;+}++/* otherwise, grow and try again (if we can) */+if(alloc==max_request_buffer)+die("request was larger than our maximum size (%lu);"+" try setting GIT_HTTP_MAX_REQUEST_BUFFER",+max_request_buffer);++alloc=alloc_nr(alloc);+if(alloc>max_request_buffer)+alloc=max_request_buffer;+REALLOC_ARRAY(buf,alloc);+}+}++staticvoidinflate_request(constchar*prog_name,intout,intbuffer_input){git_zstreamstream;+unsignedchar*full_request=NULL;unsignedcharin_buf[8192];unsignedcharout_buf[8192];unsignedlongcnt=0;
@@ -277,11 +324,21 @@ static void inflate_request(const char *prog_name, int out)git_inflate_init_gzip_only(&stream);while(1){-ssize_tn=xread(0,in_buf,sizeof(in_buf));+ssize_tn;++if(buffer_input){+if(full_request)+n=0;/* nothing left to read */+else+n=read_request(0,&full_request);+stream.next_in=full_request;+}else{+n=xread(0,in_buf,sizeof(in_buf));+stream.next_in=in_buf;+}+if(n<=0)die("request ended in the middle of the gzip stream");--stream.next_in=in_buf;stream.avail_in=n;while(0<stream.avail_in){
From: Konstantin Ryabitsev <hidden> Date: 2016-06-15 23:04:55
On 20 May 2015 at 03:37, Jeff King [off-list ref] wrote:
+ /* partial read from read_in_full means we hit EOF */
+ len += cnt;
+ if (len < alloc) {
+ *out = buf;
+ warning("request size was %lu", (unsigned long)len);
+ return len;
+ }
Jeff:
This patch appears to work well -- the only complaint I have is that I
now have "warning: request size was NNN" all over my error logs. :) Is
it supposed to convey an actual warning message, or is it merely a
debug statement?
Best,
--
Konstantin Ryabitsev
Sr. Systems Administrator
Linux Foundation Collab Projects
541-224-6067
Montréal, Québec
From: Jeff King <hidden> Date: 2016-06-15 23:04:55
On Mon, May 25, 2015 at 10:07:50PM -0400, Konstantin Ryabitsev wrote:
On 20 May 2015 at 03:37, Jeff King [off-list ref] wrote:
quoted
+ /* partial read from read_in_full means we hit EOF */
+ len += cnt;
+ if (len < alloc) {
+ *out = buf;
+ warning("request size was %lu", (unsigned long)len);
+ return len;
+ }
Jeff:
This patch appears to work well -- the only complaint I have is that I
now have "warning: request size was NNN" all over my error logs. :) Is
it supposed to convey an actual warning message, or is it merely a
debug statement?
Whoops, yeah, it was just for debugging. I missed that one when sending
out the patch.
Junio, the squashable patch is below (on jk/http-backend-deadlock-2.2),
and it looks like nothing has hit "next" yet. But you did do some
up-merging of the topic. Let me know if you would prefer to just have a
patch on top.