Skip to content

Commit bb1677f

Browse files
committed
Merge branch 'jk/reduce-malloc-in-v2-servers'
Code cleanup to limit memory consumption and tighten protocol message parsing. * jk/reduce-malloc-in-v2-servers: ls-refs: reject unknown arguments serve: reject commands used as capabilities serve: reject bogus v2 "command=ls-refs=foo" docs/protocol-v2: clarify some ls-refs ref-prefix details ls-refs: ignore very long ref-prefix counts serve: drop "keys" strvec serve: provide "receive" function for session-id capability serve: provide "receive" function for object-format capability serve: add "receive" method for v2 capabilities table serve: return capability "value" from get_capability() serve: rename is_command() to parse_command()
2 parents ddb1055 + ccf0947 commit bb1677f

File tree

4 files changed

+164
-59
lines changed

4 files changed

+164
-59
lines changed

Documentation/technical/protocol-v2.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ ls-refs takes in the following arguments:
199199
Show peeled tags.
200200
ref-prefix <prefix>
201201
When specified, only references having a prefix matching one of
202-
the provided prefixes are displayed.
202+
the provided prefixes are displayed. Multiple instances may be
203+
given, in which case references matching any prefix will be
204+
shown. Note that this is purely for optimization; a server MAY
205+
show refs not matching the prefix if it chooses, and clients
206+
should filter the result themselves.
203207

204208
If the 'unborn' feature is advertised the following argument can be
205209
included in the client's request.

ls-refs.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ static void ensure_config_read(void)
4040
config_read = 1;
4141
}
4242

43+
/*
44+
* If we see this many or more "ref-prefix" lines from the client, we consider
45+
* it "too many" and will avoid using the prefix feature entirely.
46+
*/
47+
#define TOO_MANY_PREFIXES 65536
48+
4349
/*
4450
* Check if one of the prefixes is a prefix of the ref.
4551
* If no prefixes were provided, all refs match.
@@ -158,15 +164,27 @@ int ls_refs(struct repository *r, struct packet_reader *request)
158164
data.peel = 1;
159165
else if (!strcmp("symrefs", arg))
160166
data.symrefs = 1;
161-
else if (skip_prefix(arg, "ref-prefix ", &out))
162-
strvec_push(&data.prefixes, out);
167+
else if (skip_prefix(arg, "ref-prefix ", &out)) {
168+
if (data.prefixes.nr < TOO_MANY_PREFIXES)
169+
strvec_push(&data.prefixes, out);
170+
}
163171
else if (!strcmp("unborn", arg))
164172
data.unborn = allow_unborn;
173+
else
174+
die(_("unexpected line: '%s'"), arg);
165175
}
166176

167177
if (request->status != PACKET_READ_FLUSH)
168178
die(_("expected flush after ls-refs arguments"));
169179

180+
/*
181+
* If we saw too many prefixes, we must avoid using them at all; as
182+
* soon as we have any prefix, they are meant to form a comprehensive
183+
* list.
184+
*/
185+
if (data.prefixes.nr >= TOO_MANY_PREFIXES)
186+
strvec_clear(&data.prefixes);
187+
170188
send_possibly_unborn_head(&data);
171189
if (!data.prefixes.nr)
172190
strvec_push(&data.prefixes, "");

serve.c

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "upload-pack.h"
1111

1212
static int advertise_sid = -1;
13+
static int client_hash_algo = GIT_HASH_SHA1;
1314

1415
static int always_advertise(struct repository *r,
1516
struct strbuf *value)
@@ -33,6 +34,17 @@ static int object_format_advertise(struct repository *r,
3334
return 1;
3435
}
3536

37+
static void object_format_receive(struct repository *r,
38+
const char *algo_name)
39+
{
40+
if (!algo_name)
41+
die("object-format capability requires an argument");
42+
43+
client_hash_algo = hash_algo_by_name(algo_name);
44+
if (client_hash_algo == GIT_HASH_UNKNOWN)
45+
die("unknown object format '%s'", algo_name);
46+
}
47+
3648
static int session_id_advertise(struct repository *r, struct strbuf *value)
3749
{
3850
if (advertise_sid == -1 &&
@@ -45,6 +57,14 @@ static int session_id_advertise(struct repository *r, struct strbuf *value)
4557
return 1;
4658
}
4759

60+
static void session_id_receive(struct repository *r,
61+
const char *client_sid)
62+
{
63+
if (!client_sid)
64+
client_sid = "";
65+
trace2_data_string("transfer", NULL, "client-sid", client_sid);
66+
}
67+
4868
struct protocol_capability {
4969
/*
5070
* The name of the capability. The server uses this name when
@@ -70,6 +90,16 @@ struct protocol_capability {
7090
* This field should be NULL for capabilities which are not commands.
7191
*/
7292
int (*command)(struct repository *r, struct packet_reader *request);
93+
94+
/*
95+
* Function called when a client requests the capability as a
96+
* non-command. This may be NULL if the capability does nothing.
97+
*
98+
* For a capability of the form "foo=bar", the value string points to
99+
* the content after the "=" (i.e., "bar"). For simple capabilities
100+
* (just "foo"), it is NULL.
101+
*/
102+
void (*receive)(struct repository *r, const char *value);
73103
};
74104

75105
static struct protocol_capability capabilities[] = {
@@ -94,10 +124,12 @@ static struct protocol_capability capabilities[] = {
94124
{
95125
.name = "object-format",
96126
.advertise = object_format_advertise,
127+
.receive = object_format_receive,
97128
},
98129
{
99130
.name = "session-id",
100131
.advertise = session_id_advertise,
132+
.receive = session_id_receive,
101133
},
102134
{
103135
.name = "object-info",
@@ -139,7 +171,7 @@ void protocol_v2_advertise_capabilities(void)
139171
strbuf_release(&value);
140172
}
141173

142-
static struct protocol_capability *get_capability(const char *key)
174+
static struct protocol_capability *get_capability(const char *key, const char **value)
143175
{
144176
int i;
145177

@@ -149,31 +181,46 @@ static struct protocol_capability *get_capability(const char *key)
149181
for (i = 0; i < ARRAY_SIZE(capabilities); i++) {
150182
struct protocol_capability *c = &capabilities[i];
151183
const char *out;
152-
if (skip_prefix(key, c->name, &out) && (!*out || *out == '='))
184+
if (!skip_prefix(key, c->name, &out))
185+
continue;
186+
if (!*out) {
187+
*value = NULL;
153188
return c;
189+
}
190+
if (*out++ == '=') {
191+
*value = out;
192+
return c;
193+
}
154194
}
155195

156196
return NULL;
157197
}
158198

159-
static int is_valid_capability(const char *key)
199+
static int receive_client_capability(const char *key)
160200
{
161-
const struct protocol_capability *c = get_capability(key);
201+
const char *value;
202+
const struct protocol_capability *c = get_capability(key, &value);
162203

163-
return c && c->advertise(the_repository, NULL);
204+
if (!c || c->command || !c->advertise(the_repository, NULL))
205+
return 0;
206+
207+
if (c->receive)
208+
c->receive(the_repository, value);
209+
return 1;
164210
}
165211

166-
static int is_command(const char *key, struct protocol_capability **command)
212+
static int parse_command(const char *key, struct protocol_capability **command)
167213
{
168214
const char *out;
169215

170216
if (skip_prefix(key, "command=", &out)) {
171-
struct protocol_capability *cmd = get_capability(out);
217+
const char *value;
218+
struct protocol_capability *cmd = get_capability(out, &value);
172219

173220
if (*command)
174221
die("command '%s' requested after already requesting command '%s'",
175222
out, (*command)->name);
176-
if (!cmd || !cmd->advertise(the_repository, NULL) || !cmd->command)
223+
if (!cmd || !cmd->advertise(the_repository, NULL) || !cmd->command || value)
177224
die("invalid command '%s'", out);
178225

179226
*command = cmd;
@@ -183,42 +230,6 @@ static int is_command(const char *key, struct protocol_capability **command)
183230
return 0;
184231
}
185232

186-
static int has_capability(const struct strvec *keys, const char *capability,
187-
const char **value)
188-
{
189-
int i;
190-
for (i = 0; i < keys->nr; i++) {
191-
const char *out;
192-
if (skip_prefix(keys->v[i], capability, &out) &&
193-
(!*out || *out == '=')) {
194-
if (value) {
195-
if (*out == '=')
196-
out++;
197-
*value = out;
198-
}
199-
return 1;
200-
}
201-
}
202-
203-
return 0;
204-
}
205-
206-
static void check_algorithm(struct repository *r, struct strvec *keys)
207-
{
208-
int client = GIT_HASH_SHA1, server = hash_algo_by_ptr(r->hash_algo);
209-
const char *algo_name;
210-
211-
if (has_capability(keys, "object-format", &algo_name)) {
212-
client = hash_algo_by_name(algo_name);
213-
if (client == GIT_HASH_UNKNOWN)
214-
die("unknown object format '%s'", algo_name);
215-
}
216-
217-
if (client != server)
218-
die("mismatched object format: server %s; client %s\n",
219-
r->hash_algo->name, hash_algos[client].name);
220-
}
221-
222233
enum request_state {
223234
PROCESS_REQUEST_KEYS,
224235
PROCESS_REQUEST_DONE,
@@ -228,9 +239,8 @@ static int process_request(void)
228239
{
229240
enum request_state state = PROCESS_REQUEST_KEYS;
230241
struct packet_reader reader;
231-
struct strvec keys = STRVEC_INIT;
242+
int seen_capability_or_command = 0;
232243
struct protocol_capability *command = NULL;
233-
const char *client_sid;
234244

235245
packet_reader_init(&reader, 0, NULL, 0,
236246
PACKET_READ_CHOMP_NEWLINE |
@@ -250,10 +260,9 @@ static int process_request(void)
250260
case PACKET_READ_EOF:
251261
BUG("Should have already died when seeing EOF");
252262
case PACKET_READ_NORMAL:
253-
/* collect request; a sequence of keys and values */
254-
if (is_command(reader.line, &command) ||
255-
is_valid_capability(reader.line))
256-
strvec_push(&keys, reader.line);
263+
if (parse_command(reader.line, &command) ||
264+
receive_client_capability(reader.line))
265+
seen_capability_or_command = 1;
257266
else
258267
die("unknown capability '%s'", reader.line);
259268

@@ -265,7 +274,7 @@ static int process_request(void)
265274
* If no command and no keys were given then the client
266275
* wanted to terminate the connection.
267276
*/
268-
if (!keys.nr)
277+
if (!seen_capability_or_command)
269278
return 1;
270279

271280
/*
@@ -292,14 +301,13 @@ static int process_request(void)
292301
if (!command)
293302
die("no command requested");
294303

295-
check_algorithm(the_repository, &keys);
296-
297-
if (has_capability(&keys, "session-id", &client_sid))
298-
trace2_data_string("transfer", NULL, "client-sid", client_sid);
304+
if (client_hash_algo != hash_algo_by_ptr(the_repository->hash_algo))
305+
die("mismatched object format: server %s; client %s\n",
306+
the_repository->hash_algo->name,
307+
hash_algos[client_hash_algo].name);
299308

300309
command->command(the_repository, &reader);
301310

302-
strvec_clear(&keys);
303311
return 0;
304312
}
305313

t/t5701-git-serve.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,37 @@ test_expect_success 'request invalid command' '
7272
test_i18ngrep "invalid command" err
7373
'
7474

75+
test_expect_success 'request capability as command' '
76+
test-tool pkt-line pack >in <<-EOF &&
77+
command=agent
78+
object-format=$(test_oid algo)
79+
0000
80+
EOF
81+
test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
82+
grep invalid.command.*agent err
83+
'
84+
85+
test_expect_success 'request command as capability' '
86+
test-tool pkt-line pack >in <<-EOF &&
87+
command=ls-refs
88+
object-format=$(test_oid algo)
89+
fetch
90+
0000
91+
EOF
92+
test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
93+
grep unknown.capability err
94+
'
95+
96+
test_expect_success 'requested command is command=value' '
97+
test-tool pkt-line pack >in <<-EOF &&
98+
command=ls-refs=whatever
99+
object-format=$(test_oid algo)
100+
0000
101+
EOF
102+
test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
103+
grep invalid.command.*ls-refs=whatever err
104+
'
105+
75106
test_expect_success 'wrong object-format' '
76107
test-tool pkt-line pack >in <<-EOF &&
77108
command=fetch
@@ -116,6 +147,19 @@ test_expect_success 'basics of ls-refs' '
116147
test_cmp expect actual
117148
'
118149

150+
test_expect_success 'ls-refs complains about unknown options' '
151+
test-tool pkt-line pack >in <<-EOF &&
152+
command=ls-refs
153+
object-format=$(test_oid algo)
154+
0001
155+
no-such-arg
156+
0000
157+
EOF
158+
159+
test_must_fail test-tool serve-v2 --stateless-rpc 2>err <in &&
160+
grep unexpected.line.*no-such-arg err
161+
'
162+
119163
test_expect_success 'basic ref-prefixes' '
120164
test-tool pkt-line pack >in <<-EOF &&
121165
command=ls-refs
@@ -158,6 +202,37 @@ test_expect_success 'refs/heads prefix' '
158202
test_cmp expect actual
159203
'
160204

205+
test_expect_success 'ignore very large set of prefixes' '
206+
# generate a large number of ref-prefixes that we expect
207+
# to match nothing; the value here exceeds TOO_MANY_PREFIXES
208+
# from ls-refs.c.
209+
{
210+
echo command=ls-refs &&
211+
echo object-format=$(test_oid algo) &&
212+
echo 0001 &&
213+
perl -le "print \"ref-prefix refs/heads/\$_\" for (1..65536)" &&
214+
echo 0000
215+
} |
216+
test-tool pkt-line pack >in &&
217+
218+
# and then confirm that we see unmatched prefixes anyway (i.e.,
219+
# that the prefix was not applied).
220+
cat >expect <<-EOF &&
221+
$(git rev-parse HEAD) HEAD
222+
$(git rev-parse refs/heads/dev) refs/heads/dev
223+
$(git rev-parse refs/heads/main) refs/heads/main
224+
$(git rev-parse refs/heads/release) refs/heads/release
225+
$(git rev-parse refs/tags/annotated-tag) refs/tags/annotated-tag
226+
$(git rev-parse refs/tags/one) refs/tags/one
227+
$(git rev-parse refs/tags/two) refs/tags/two
228+
0000
229+
EOF
230+
231+
test-tool serve-v2 --stateless-rpc <in >out &&
232+
test-tool pkt-line unpack <out >actual &&
233+
test_cmp expect actual
234+
'
235+
161236
test_expect_success 'peel parameter' '
162237
test-tool pkt-line pack >in <<-EOF &&
163238
command=ls-refs

0 commit comments

Comments
 (0)