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
|
||||
(empty) response, the output of the service starts. Messages
|
||||
(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
|
||||
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
|
||||
* '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
|
||||
----------------------
|
||||
|
@ -224,7 +224,7 @@ int cmd_fetch_pack(int argc, const char **argv, const char *prefix)
|
||||
version = discover_version(&reader);
|
||||
switch (version) {
|
||||
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;
|
||||
case protocol_v1:
|
||||
case protocol_v0:
|
||||
|
16
connect.c
16
connect.c
@ -406,10 +406,21 @@ out:
|
||||
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 **list, int for_push,
|
||||
const struct argv_array *ref_prefixes,
|
||||
const struct string_list *server_options)
|
||||
const struct string_list *server_options,
|
||||
int stateless_rpc)
|
||||
{
|
||||
int i;
|
||||
*list = NULL;
|
||||
@ -446,6 +457,9 @@ struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
|
||||
if (reader->status != PACKET_READ_FLUSH)
|
||||
die(_("expected flush after ref listing"));
|
||||
|
||||
check_stateless_delimiter(stateless_rpc, reader,
|
||||
_("expected response end packet after ref listing"));
|
||||
|
||||
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 die_on_error);
|
||||
|
||||
void check_stateless_delimiter(int stateless_rpc,
|
||||
struct packet_reader *reader,
|
||||
const char *error);
|
||||
|
||||
#endif
|
||||
|
13
fetch-pack.c
13
fetch-pack.c
@ -1451,6 +1451,13 @@ enum fetch_state {
|
||||
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,
|
||||
int fd[2],
|
||||
const struct ref *orig_ref,
|
||||
@ -1535,6 +1542,10 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||
/* Process ACKs/NAKs */
|
||||
switch (process_acks(negotiator, &reader, &common)) {
|
||||
case READY:
|
||||
/*
|
||||
* Don't check for response delimiter; get_pack() will
|
||||
* read the rest of this response.
|
||||
*/
|
||||
state = FETCH_GET_PACK;
|
||||
break;
|
||||
case COMMON_FOUND:
|
||||
@ -1542,6 +1553,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||
seen_ack = 1;
|
||||
/* fallthrough */
|
||||
case NO_COMMON_FOUND:
|
||||
do_check_stateless_delimiter(args, &reader);
|
||||
state = FETCH_SEND_REQUEST;
|
||||
break;
|
||||
}
|
||||
@ -1561,6 +1573,7 @@ static struct ref *do_fetch_pack_v2(struct fetch_pack_args *args,
|
||||
process_section_header(&reader, "packfile", 0);
|
||||
if (get_pack(args, fd, pack_lockfile, sought, nr_sought))
|
||||
die(_("git fetch-pack: fetch failed."));
|
||||
do_check_stateless_delimiter(args, &reader);
|
||||
|
||||
state = FETCH_DONE;
|
||||
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);
|
||||
if (state->remaining < 0) {
|
||||
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) {
|
||||
state->remaining = 0;
|
||||
} else {
|
||||
@ -991,6 +993,9 @@ retry:
|
||||
if (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);
|
||||
free(gzip_body);
|
||||
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 **list, int for_push,
|
||||
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);
|
||||
|
||||
|
@ -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_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_when_finished "rm -f log" &&
|
||||
|
||||
|
@ -297,7 +297,8 @@ static struct ref *handshake(struct transport *transport, int for_push,
|
||||
if (must_list_refs)
|
||||
get_remote_refs(data->fd[1], &reader, &refs, for_push,
|
||||
ref_prefixes,
|
||||
transport->server_options);
|
||||
transport->server_options,
|
||||
transport->stateless_rpc);
|
||||
break;
|
||||
case protocol_v1:
|
||||
case protocol_v0:
|
||||
|
Loading…
Reference in New Issue
Block a user