reftable/stack: fix segfault when reload with reused readers fails

It is expected that reloading the stack fails with concurrent writers, e.g. because a table that we just wanted to read just got compacted. In case we decided to reuse readers this will cause a segfault though because we unconditionally release all new readers, including the reused ones. As those are still referenced by the current stack, the result is that we will eventually try to dereference those already-freed readers. Fix this bug by incrementing the refcount of reused readers temporarily. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Aug 23, 2024 at 16:12 UTC 85da2a2ab62e24a8b9ff183fe3a451b445632487
2 files changed +82
reftable/stack.c
+23
@@ -226,6 +226,8 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
226 {
227 size_t cur_len = !st->merged ? 0 : st->merged->readers_len;
228 struct reftable_reader **cur = stack_copy_readers(st, cur_len);
229 + struct reftable_reader **reused = NULL;
230 + size_t reused_len = 0, reused_alloc = 0;
231 size_t names_len = names_length(names);
232 struct reftable_reader **new_readers =
233 reftable_calloc(names_len, sizeof(*new_readers));
@@ -245,6 +247,18 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
247 if (cur[i] && 0 == strcmp(cur[i]->name, name)) {
248 rd = cur[i];
249 cur[i] = NULL;
250 +
251 + /*
252 + * When reloading the stack fails, we end up
253 + * releasing all new readers. This also
254 + * includes the reused readers, even though
255 + * they are still in used by the old stack. We
256 + * thus need to keep them alive here, which we
257 + * do by bumping their refcount.
258 + */
259 + REFTABLE_ALLOC_GROW(reused, reused_len + 1, reused_alloc);
260 + reused[reused_len++] = rd;
261 + reftable_reader_incref(rd);
262 break;
263 }
264 }
@@ -301,10 +315,19 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
315 new_readers = NULL;
316 new_readers_len = 0;
317
318 + /*
319 + * Decrement the refcount of reused readers again. This only needs to
320 + * happen on the successful case, because on the unsuccessful one we
321 + * decrement their refcount via `new_readers`.
322 + */
323 + for (i = 0; i < reused_len; i++)
324 + reftable_reader_decref(reused[i]);
325 +
326 done:
327 for (i = 0; i < new_readers_len; i++)
328 reftable_reader_decref(new_readers[i]);
329 reftable_free(new_readers);
330 + reftable_free(reused);
331 reftable_free(cur);
332 strbuf_release(&table_path);
333 return err;
reftable/stack_test.c
+59
@@ -10,6 +10,7 @@ https://developers.google.com/open-source/licenses/bsd
10
11 #include "system.h"
12
13 +#include "copy.h"
14 #include "reftable-reader.h"
15 #include "merged.h"
16 #include "basics.h"
@@ -1125,6 +1126,63 @@ static void test_reftable_stack_read_across_reload(void)
1126 clear_dir(dir);
1127 }
1128
1129 +static void test_reftable_stack_reload_with_missing_table(void)
1130 +{
1131 + struct reftable_write_options opts = { 0 };
1132 + struct reftable_stack *st = NULL;
1133 + struct reftable_ref_record rec = { 0 };
1134 + struct reftable_iterator it = { 0 };
1135 + struct strbuf table_path = STRBUF_INIT, content = STRBUF_INIT;
1136 + char *dir = get_tmp_dir(__LINE__);
1137 + int err;
1138 +
1139 + /* Create a first stack and set up an iterator for it. */
1140 + err = reftable_new_stack(&st, dir, &opts);
1141 + EXPECT_ERR(err);
1142 + write_n_ref_tables(st, 2);
1143 + EXPECT(st->merged->readers_len == 2);
1144 + reftable_stack_init_ref_iterator(st, &it);
1145 + err = reftable_iterator_seek_ref(&it, "");
1146 + EXPECT_ERR(err);
1147 +
1148 + /*
1149 + * Update the tables.list file with some garbage data, while reusing
1150 + * our old readers. This should trigger a partial reload of the stack,
1151 + * where we try to reuse our old readers.
1152 + */
1153 + strbuf_addf(&content, "%s\n", st->readers[0]->name);
1154 + strbuf_addf(&content, "%s\n", st->readers[1]->name);
1155 + strbuf_addstr(&content, "garbage\n");
1156 + strbuf_addf(&table_path, "%s.lock", st->list_file);
1157 + write_file_buf(table_path.buf, content.buf, content.len);
1158 + err = rename(table_path.buf, st->list_file);
1159 + EXPECT_ERR(err);
1160 +
1161 + err = reftable_stack_reload(st);
1162 + EXPECT(err == -4);
1163 + EXPECT(st->merged->readers_len == 2);
1164 +
1165 + /*
1166 + * Even though the reload has failed, we should be able to continue
1167 + * using the iterator.
1168 + */
1169 + err = reftable_iterator_next_ref(&it, &rec);
1170 + EXPECT_ERR(err);
1171 + EXPECT(!strcmp(rec.refname, "refs/heads/branch-0000"));
1172 + err = reftable_iterator_next_ref(&it, &rec);
1173 + EXPECT_ERR(err);
1174 + EXPECT(!strcmp(rec.refname, "refs/heads/branch-0001"));
1175 + err = reftable_iterator_next_ref(&it, &rec);
1176 + EXPECT(err > 0);
1177 +
1178 + reftable_ref_record_release(&rec);
1179 + reftable_iterator_destroy(&it);
1180 + reftable_stack_destroy(st);
1181 + strbuf_release(&table_path);
1182 + strbuf_release(&content);
1183 + clear_dir(dir);
1184 +}
1185 +
1186 int stack_test_main(int argc, const char *argv[])
1187 {
1188 RUN_TEST(test_empty_add);
@@ -1148,6 +1206,7 @@ int stack_test_main(int argc, const char *argv[])
1206 RUN_TEST(test_reftable_stack_update_index_check);
1207 RUN_TEST(test_reftable_stack_uptodate);
1208 RUN_TEST(test_reftable_stack_read_across_reload);
1209 + RUN_TEST(test_reftable_stack_reload_with_missing_table);
1210 RUN_TEST(test_suggest_compaction_segment);
1211 RUN_TEST(test_suggest_compaction_segment_nothing);
1212 return 0;