mirror of https://github.com/git/git.git
stateless-connect: send response end packet
Currently, remote-curl acts as a proxy and blindly forwards packets between an HTTP server and fetch-pack. In the case of a stateless RPC connection where the connection is terminated before the transaction is complete, remote-curl will blindly forward the packets before waiting on more input from fetch-pack. Meanwhile, fetch-pack will read the transaction and continue reading, expecting more input to continue the transaction. This results in a deadlock between the two processes. This can be seen in the following command which does not terminate: $ git -c protocol.version=2 clone https://github.com/git/git.git --shallow-since=20151012 Cloning into 'git'... whereas the v1 version does terminate as expected: $ git -c protocol.version=1 clone https://github.com/git/git.git --shallow-since=20151012 Cloning into 'git'... fatal: the remote end hung up unexpectedly Instead of blindly forwarding packets, make remote-curl insert a response end packet after proxying the responses from the remote server when using stateless_connect(). On the RPC client side, ensure that each response ends as described. A separate control packet is chosen because we need to be able to differentiate between what the remote server sends and remote-curl's control packets. By ensuring in the remote-curl code that a server cannot send response end packets, we prevent a malicious server from being able to perform a denial of service attack in which they spoof a response end packet and cause the described deadlock to happen. Reported-by: Force Charlie <charlieio@outlook.com> Helped-by: Jeff King <peff@peff.net> Signed-off-by: Denton Liu <liu.denton@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
parent
0181b600a6
commit
b0df0c16ea
|
@ -405,7 +405,9 @@ Supported if the helper has the "connect" capability.
|
||||||
trying to fall back). After line feed terminating the positive
|
trying to fall back). After line feed terminating the positive
|
||||||
(empty) response, the output of the service starts. Messages
|
(empty) response, the output of the service starts. Messages
|
||||||
(both request and response) must consist of zero or more
|
(both request and response) must consist of zero or more
|
||||||
PKT-LINEs, terminating in a flush packet. The client must not
|
PKT-LINEs, terminating in a flush packet. Response messages will
|
||||||
|
then have a response end packet after the flush packet to
|
||||||
|
indicate the end of a response. The client must not
|
||||||
expect the server to store any state in between request-response
|
expect the server to store any state in between request-response
|
||||||
pairs. After the connection ends, the remote helper exits.
|
pairs. After the connection ends, the remote helper exits.
|
||||||
+
|
+
|
||||||
|
|
|
@ -33,6 +33,8 @@ In protocol v2 these special packets will have the following semantics:
|
||||||
|
|
||||||
* '0000' Flush Packet (flush-pkt) - indicates the end of a message
|
* '0000' Flush Packet (flush-pkt) - indicates the end of a message
|
||||||
* '0001' Delimiter Packet (delim-pkt) - separates sections of a message
|
* '0001' Delimiter Packet (delim-pkt) - separates sections of a message
|
||||||
|
* '0002' Message Packet (response-end-pkt) - indicates the end of a response
|
||||||
|
for stateless connections
|
||||||
|
|
||||||
Initial Client Request
|
Initial Client Request
|
||||||
----------------------
|
----------------------
|
||||||
|
|
|
@ -224,7 +224,7 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)
|
||||||
version = discover_version(&reader);
|
version = discover_version(&reader);
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case protocol_v2:
|
case protocol_v2:
|
||||||
get_remote_refs(fd[1], &reader, &ref, 0, NULL, NULL);
|
get_remote_refs(fd[1], &reader, &ref, 0, NULL, NULL, args.stateless_rpc);
|
||||||
break;
|
break;
|
||||||
case protocol_v1:
|
case protocol_v1:
|
||||||
case protocol_v0:
|
case protocol_v0:
|
||||||
|
|
16
connect.c
16
connect.c
|
@ -406,10 +406,21 @@ out:
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void check_stateless_delimiter(int stateless_rpc,
|
||||||
|
struct packet_reader *reader,
|
||||||
|
const char *error)
|
||||||
|
{
|
||||||
|
if (!stateless_rpc)
|
||||||
|
return; /* not in stateless mode, no delimiter expected */
|
||||||
|
if (packet_reader_read(reader) != PACKET_READ_RESPONSE_END)
|
||||||
|
die("%s", error);
|
||||||
|
}
|
||||||
|
|
||||||
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
||||||
struct ref **list, int for_push,
|
struct ref **list, int for_push,
|
||||||
const struct argv_array *ref_prefixes,
|
const struct argv_array *ref_prefixes,
|
||||||
const struct string_list *server_options)
|
const struct string_list *server_options,
|
||||||
|
int stateless_rpc)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
*list = NULL;
|
*list = NULL;
|
||||||
|
@ -446,6 +457,9 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
||||||
if (reader->status != PACKET_READ_FLUSH)
|
if (reader->status != PACKET_READ_FLUSH)
|
||||||
die(_("expected flush after ref listing"));
|
die(_("expected flush after ref listing"));
|
||||||
|
|
||||||
|
check_stateless_delimiter(stateless_rpc, reader,
|
||||||
|
_("expected response end packet after ref listing"));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,4 +22,8 @@ int server_supports_v2(const char *c, int die_on_error);
|
||||||
int server_supports_feature(const char *c, const char *feature,
|
int server_supports_feature(const char *c, const char *feature,
|
||||||
int die_on_error);
|
int die_on_error);
|
||||||
|
|
||||||
|
void check_stateless_delimiter(int stateless_rpc,
|
||||||
|
struct packet_reader *reader,
|
||||||
|
const char *error);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
13
fetch-pack.c
13
fetch-pack.c
|
@ -1451,6 +1451,13 @@ enum fetch_state {
|
||||||
FETCH_DONE,
|
FETCH_DONE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void do_check_stateless_delimiter(const struct fetch_pack_args *args,
|
||||||
|
struct packet_reader *reader)
|
||||||
|
{
|
||||||
|
check_stateless_delimiter(args->stateless_rpc, reader,
|
||||||
|
_("git fetch-pack: expected response end packet"));
|
||||||
|
}
|
||||||
|
|
||||||
static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||||
int fd[2],
|
int fd[2],
|
||||||
const struct ref *orig_ref,
|
const struct ref *orig_ref,
|
||||||
|
@ -1535,6 +1542,10 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||||
/* Process ACKs/NAKs */
|
/* Process ACKs/NAKs */
|
||||||
switch (process_acks(negotiator, &reader, &common)) {
|
switch (process_acks(negotiator, &reader, &common)) {
|
||||||
case READY:
|
case READY:
|
||||||
|
/*
|
||||||
|
* Don't check for response delimiter; get_pack() will
|
||||||
|
* read the rest of this response.
|
||||||
|
*/
|
||||||
state = FETCH_GET_PACK;
|
state = FETCH_GET_PACK;
|
||||||
break;
|
break;
|
||||||
case COMMON_FOUND:
|
case COMMON_FOUND:
|
||||||
|
@ -1542,6 +1553,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||||
seen_ack = 1;
|
seen_ack = 1;
|
||||||
/* fallthrough */
|
/* fallthrough */
|
||||||
case NO_COMMON_FOUND:
|
case NO_COMMON_FOUND:
|
||||||
|
do_check_stateless_delimiter(args, &reader);
|
||||||
state = FETCH_SEND_REQUEST;
|
state = FETCH_SEND_REQUEST;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1561,6 +1573,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||||
process_section_header(&reader, "packfile", 0);
|
process_section_header(&reader, "packfile", 0);
|
||||||
if (get_pack(args, fd, pack_lockfile, sought, nr_sought))
|
if (get_pack(args, fd, pack_lockfile, sought, nr_sought))
|
||||||
die(_("git fetch-pack: fetch failed."));
|
die(_("git fetch-pack: fetch failed."));
|
||||||
|
do_check_stateless_delimiter(args, &reader);
|
||||||
|
|
||||||
state = FETCH_DONE;
|
state = FETCH_DONE;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -703,6 +703,8 @@ static void check_pktline(struct check_pktline_state *state, const char *ptr, si
|
||||||
state->remaining = packet_length(state->len_buf);
|
state->remaining = packet_length(state->len_buf);
|
||||||
if (state->remaining < 0) {
|
if (state->remaining < 0) {
|
||||||
die(_("remote-curl: bad line length character: %.4s"), state->len_buf);
|
die(_("remote-curl: bad line length character: %.4s"), state->len_buf);
|
||||||
|
} else if (state->remaining == 2) {
|
||||||
|
die(_("remote-curl: unexpected response end packet"));
|
||||||
} else if (state->remaining < 4) {
|
} else if (state->remaining < 4) {
|
||||||
state->remaining = 0;
|
state->remaining = 0;
|
||||||
} else {
|
} else {
|
||||||
|
@ -991,6 +993,9 @@ retry:
|
||||||
if (rpc_in_data.pktline_state.remaining)
|
if (rpc_in_data.pktline_state.remaining)
|
||||||
err = error(_("%d bytes of body are still expected"), rpc_in_data.pktline_state.remaining);
|
err = error(_("%d bytes of body are still expected"), rpc_in_data.pktline_state.remaining);
|
||||||
|
|
||||||
|
if (stateless_connect)
|
||||||
|
packet_response_end(rpc->in);
|
||||||
|
|
||||||
curl_slist_free_all(headers);
|
curl_slist_free_all(headers);
|
||||||
free(gzip_body);
|
free(gzip_body);
|
||||||
return err;
|
return err;
|
||||||
|
|
3
remote.h
3
remote.h
|
@ -179,7 +179,8 @@ struct ref **get_remote_heads(struct packet_reader *reader,
|
||||||
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
||||||
struct ref **list, int for_push,
|
struct ref **list, int for_push,
|
||||||
const struct argv_array *ref_prefixes,
|
const struct argv_array *ref_prefixes,
|
||||||
const struct string_list *server_options);
|
const struct string_list *server_options,
|
||||||
|
int stateless_rpc);
|
||||||
|
|
||||||
int resolve_remote_symref(struct ref *ref, struct ref *list);
|
int resolve_remote_symref(struct ref *ref, struct ref *list);
|
||||||
|
|
||||||
|
|
|
@ -620,6 +620,19 @@ test_expect_success 'clone repository with http:// using protocol v2 with incomp
|
||||||
test_i18ngrep "bytes of body are still expected" err
|
test_i18ngrep "bytes of body are still expected" err
|
||||||
'
|
'
|
||||||
|
|
||||||
|
test_expect_success 'clone with http:// using protocol v2 and invalid parameters' '
|
||||||
|
test_when_finished "rm -f log" &&
|
||||||
|
|
||||||
|
test_must_fail env GIT_TRACE_PACKET="$(pwd)/log" GIT_TRACE_CURL="$(pwd)/log" \
|
||||||
|
git -c protocol.version=2 \
|
||||||
|
clone --shallow-since=20151012 "$HTTPD_URL/smart/http_parent" http_child_invalid &&
|
||||||
|
|
||||||
|
# Client requested to use protocol v2
|
||||||
|
grep "Git-Protocol: version=2" log &&
|
||||||
|
# Server responded using protocol v2
|
||||||
|
grep "git< version 2" log
|
||||||
|
'
|
||||||
|
|
||||||
test_expect_success 'clone big repository with http:// using protocol v2' '
|
test_expect_success 'clone big repository with http:// using protocol v2' '
|
||||||
test_when_finished "rm -f log" &&
|
test_when_finished "rm -f log" &&
|
||||||
|
|
||||||
|
|
|
@ -297,7 +297,8 @@ static struct ref *handshake(struct transport *transport, int for_push,
|
||||||
if (must_list_refs)
|
if (must_list_refs)
|
||||||
get_remote_refs(data->fd[1], &reader, &refs, for_push,
|
get_remote_refs(data->fd[1], &reader, &refs, for_push,
|
||||||
ref_prefixes,
|
ref_prefixes,
|
||||||
transport->server_options);
|
transport->server_options,
|
||||||
|
transport->stateless_rpc);
|
||||||
break;
|
break;
|
||||||
case protocol_v1:
|
case protocol_v1:
|
||||||
case protocol_v0:
|
case protocol_v0:
|
||||||
|
|
Loading…
Reference in New Issue