Skip to content

Commit b7dcea0

Browse files
committed
config_entries: micro-optimize storage of multivars
Multivars are configuration entries that have many values for the same name; we can thus micro-optimize this case by just retaining the name of the first configuration entry and freeing all the others, letting them point to the string of the first entry. The attached test case is an extreme example that demonstrates this. It contains a section name that is approximately 500kB in size with 20.000 entries "a=b". Without the optimization, this would require at least 20000*500kB bytes, which is around 10GB. With this patch, it only requires 500kB+20000*1B=20500kB. The obvious culprit here is the section header, which we repeatedly include in each of the configuration entry's names. This makes it very easier for an adversary to provide a small configuration file that disproportionally blows up in memory during processing and is thus a feasible way for a denial-of-service attack. Unfortunately, we cannot fix the root cause by e.g. having a separate "section" field that may easily be deduplicated due to the `git_config_entry` structure being part of our public API. So this micro-optimization is the best we can do for now.
1 parent 6232086 commit b7dcea0

File tree

3 files changed

+40036
-3
lines changed

3 files changed

+40036
-3
lines changed

src/config_entries.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ static void config_entries_free(git_config_entries *entries)
108108
list = entries->list;
109109
while (list != NULL) {
110110
next = list->next;
111-
git__free((char *) list->entry->name);
111+
if (list->first)
112+
git__free((char *) list->entry->name);
112113
git__free((char *) list->entry->value);
113114
git__free(list->entry);
114115
git__free(list);
@@ -126,12 +127,24 @@ void git_config_entries_free(git_config_entries *entries)
126127

127128
int git_config_entries_append(git_config_entries *entries, git_config_entry *entry)
128129
{
129-
config_entry_list *head;
130+
config_entry_list *existing, *head;
130131

131132
head = git__calloc(1, sizeof(config_entry_list));
132133
GIT_ERROR_CHECK_ALLOC(head);
133134
head->entry = entry;
134-
head->first = (git_strmap_get(entries->map, entry->name) == NULL);
135+
136+
/*
137+
* This is a micro-optimization for configuration files
138+
* with a lot of same keys. As for multivars the entry's
139+
* key will be the same for all entries, we can just free
140+
* all except the first entry's name and just re-use it.
141+
*/
142+
if ((existing = git_strmap_get(entries->map, entry->name)) != NULL) {
143+
git__free((char *) entry->name);
144+
entry->name = existing->entry->name;
145+
} else {
146+
head->first = 1;
147+
}
135148

136149
if (entries->list)
137150
entries->list->last->next = head;

tests/config/stress.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,23 @@ void test_config_stress__foreach_refreshes_snapshot(void)
176176
git_config_free(config);
177177
git__free(value);
178178
}
179+
180+
void test_config_stress__huge_section_with_many_values(void)
181+
{
182+
git_config *config;
183+
184+
/*
185+
* The config file is structured in such a way that is
186+
* has a section header that is approximately 500kb of
187+
* size followed by 40k entries. While the resulting
188+
* configuration file itself is roughly 650kb in size and
189+
* thus considered to be rather small, in the past we'd
190+
* balloon to more than 20GB of memory (20000x500kb)
191+
* while parsing the file. It thus was a trivial way to
192+
* cause an out-of-memory situation and thus cause denial
193+
* of service, e.g. via gitmodules.
194+
*/
195+
cl_git_pass(git_config_open_ondisk(&config, cl_fixture("config/config-oom")));
196+
197+
git_config_free(config);
198+
}

tests/resources/config/config-oom

Lines changed: 40000 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)