* [PATCH 0/4] reftable: fix realloc error handling
@ 2024-12-25 18:33 René Scharfe
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
` (5 more replies)
0 siblings, 6 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-25 18:33 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
The current handling of reallocation errors leaks the original
allocation in most cases and corrupts the capacity variable. Fix
that in REFTABLE_ALLOC_GROW and by providing a new macro
REFTABLE_ALLOC_GROW_OR_NULL -- solve this somewhat tricky issue
centrally, with minimal impact to calling code.
And the last two patches add error handling to the remaining
places that still lack it.
reftable: avoid leaks on realloc error
reftable: fix allocation count on realloc error
reftable: handle realloc error in parse_names()
t-reftable-merged: handle realloc errors
reftable/basics.c | 14 ++++-------
reftable/basics.h | 41 +++++++++++++++++++++++++-------
reftable/block.c | 10 ++++----
reftable/pq.c | 2 +-
reftable/record.c | 12 +++++-----
reftable/stack.c | 8 ++++---
reftable/writer.c | 5 ++--
t/unit-tests/t-reftable-merged.c | 4 ++--
8 files changed, 60 insertions(+), 36 deletions(-)
--
2.47.1
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH 1/4] reftable: avoid leaks on realloc error
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
@ 2024-12-25 18:38 ` René Scharfe
2024-12-27 5:39 ` Junio C Hamano
2024-12-27 10:33 ` Patrick Steinhardt
2024-12-25 18:38 ` [PATCH 2/4] reftable: fix allocation count " René Scharfe
` (4 subsequent siblings)
5 siblings, 2 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-25 18:38 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
When realloc(3) fails, it returns NULL and keeps the original allocation
intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
the allocation count variable in that case, simultaneously leaking the
original allocation and misrepresenting the number of storable items.
parse_names() and reftable_buf_add() avoid leaking by restoring the
original pointer value on failure, but all other callers seem to be OK
with losing the old allocation. Add a new variant of the macro,
REFTABLE_ALLOC_GROW_OR_NULL, which plugs the leak and zeros the
allocation counter. Use it for those callers.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.h | 10 ++++++++++
reftable/block.c | 10 ++++++----
reftable/pq.c | 2 +-
reftable/record.c | 12 ++++++------
reftable/stack.c | 8 +++++---
reftable/writer.c | 5 +++--
6 files changed, 31 insertions(+), 16 deletions(-)
diff --git a/reftable/basics.h b/reftable/basics.h
index 36beda2c25..259f4c274c 100644
--- a/reftable/basics.h
+++ b/reftable/basics.h
@@ -129,6 +129,16 @@ char *reftable_strdup(const char *str);
REFTABLE_REALLOC_ARRAY(x, alloc); \
} \
} while (0)
+
+#define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
+ void *reftable_alloc_grow_or_null_orig_ptr = (x); \
+ REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
+ if (!(x)) { \
+ reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
+ alloc = 0; \
+ } \
+} while (0)
+
#define REFTABLE_FREE_AND_NULL(p) do { reftable_free(p); (p) = NULL; } while (0)
#ifndef REFTABLE_ALLOW_BANNED_ALLOCATORS
diff --git a/reftable/block.c b/reftable/block.c
index 0198078485..9858bbc7c5 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -53,7 +53,8 @@ static int block_writer_register_restart(struct block_writer *w, int n,
if (2 + 3 * rlen + n > w->block_size - w->next)
return -1;
if (is_restart) {
- REFTABLE_ALLOC_GROW(w->restarts, w->restart_len + 1, w->restart_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->restarts, w->restart_len + 1,
+ w->restart_cap);
if (!w->restarts)
return REFTABLE_OUT_OF_MEMORY_ERROR;
w->restarts[w->restart_len++] = w->next;
@@ -176,7 +177,8 @@ int block_writer_finish(struct block_writer *w)
* is guaranteed to return `Z_STREAM_END`.
*/
compressed_len = deflateBound(w->zstream, src_len);
- REFTABLE_ALLOC_GROW(w->compressed, compressed_len, w->compressed_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->compressed, compressed_len,
+ w->compressed_cap);
if (!w->compressed) {
ret = REFTABLE_OUT_OF_MEMORY_ERROR;
return ret;
@@ -235,8 +237,8 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
uLong src_len = block->len - block_header_skip;
/* Log blocks specify the *uncompressed* size in their header. */
- REFTABLE_ALLOC_GROW(br->uncompressed_data, sz,
- br->uncompressed_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(br->uncompressed_data, sz,
+ br->uncompressed_cap);
if (!br->uncompressed_data) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/pq.c b/reftable/pq.c
index 6ee1164dd3..5591e875e1 100644
--- a/reftable/pq.c
+++ b/reftable/pq.c
@@ -49,7 +49,7 @@ int merged_iter_pqueue_add(struct merged_iter_pqueue *pq, const struct pq_entry
{
size_t i = 0;
- REFTABLE_ALLOC_GROW(pq->heap, pq->len + 1, pq->cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(pq->heap, pq->len + 1, pq->cap);
if (!pq->heap)
return REFTABLE_OUT_OF_MEMORY_ERROR;
pq->heap[pq->len++] = *e;
diff --git a/reftable/record.c b/reftable/record.c
index fb5652ed57..04429d23fe 100644
--- a/reftable/record.c
+++ b/reftable/record.c
@@ -246,8 +246,8 @@ static int reftable_ref_record_copy_from(void *rec, const void *src_rec,
if (src->refname) {
size_t refname_len = strlen(src->refname);
- REFTABLE_ALLOC_GROW(ref->refname, refname_len + 1,
- ref->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(ref->refname, refname_len + 1,
+ ref->refname_cap);
if (!ref->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto out;
@@ -385,7 +385,7 @@ static int reftable_ref_record_decode(void *rec, struct reftable_buf key,
SWAP(r->refname, refname);
SWAP(r->refname_cap, refname_cap);
- REFTABLE_ALLOC_GROW(r->refname, key.len + 1, r->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->refname, key.len + 1, r->refname_cap);
if (!r->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -839,7 +839,7 @@ static int reftable_log_record_decode(void *rec, struct reftable_buf key,
if (key.len <= 9 || key.buf[key.len - 9] != 0)
return REFTABLE_FORMAT_ERROR;
- REFTABLE_ALLOC_GROW(r->refname, key.len - 8, r->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->refname, key.len - 8, r->refname_cap);
if (!r->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -947,8 +947,8 @@ static int reftable_log_record_decode(void *rec, struct reftable_buf key,
}
string_view_consume(&in, n);
- REFTABLE_ALLOC_GROW(r->value.update.message, scratch->len + 1,
- r->value.update.message_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->value.update.message, scratch->len + 1,
+ r->value.update.message_cap);
if (!r->value.update.message) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/stack.c b/reftable/stack.c
index 634f0c5425..531660a49f 100644
--- a/reftable/stack.c
+++ b/reftable/stack.c
@@ -317,7 +317,9 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
* thus need to keep them alive here, which we
* do by bumping their refcount.
*/
- REFTABLE_ALLOC_GROW(reused, reused_len + 1, reused_alloc);
+ REFTABLE_ALLOC_GROW_OR_NULL(reused,
+ reused_len + 1,
+ reused_alloc);
if (!reused) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -949,8 +951,8 @@ int reftable_addition_add(struct reftable_addition *add,
if (err < 0)
goto done;
- REFTABLE_ALLOC_GROW(add->new_tables, add->new_tables_len + 1,
- add->new_tables_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(add->new_tables, add->new_tables_len + 1,
+ add->new_tables_cap);
if (!add->new_tables) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/writer.c b/reftable/writer.c
index 624e90fb53..740c98038e 100644
--- a/reftable/writer.c
+++ b/reftable/writer.c
@@ -254,7 +254,8 @@ static int writer_index_hash(struct reftable_writer *w, struct reftable_buf *has
if (key->offset_len > 0 && key->offsets[key->offset_len - 1] == off)
return 0;
- REFTABLE_ALLOC_GROW(key->offsets, key->offset_len + 1, key->offset_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(key->offsets, key->offset_len + 1,
+ key->offset_cap);
if (!key->offsets)
return REFTABLE_OUT_OF_MEMORY_ERROR;
key->offsets[key->offset_len++] = off;
@@ -820,7 +821,7 @@ static int writer_flush_nonempty_block(struct reftable_writer *w)
* Note that this also applies when flushing index blocks, in which
* case we will end up with a multi-level index.
*/
- REFTABLE_ALLOC_GROW(w->index, w->index_len + 1, w->index_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->index, w->index_len + 1, w->index_cap);
if (!w->index)
return REFTABLE_OUT_OF_MEMORY_ERROR;
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH 2/4] reftable: fix allocation count on realloc error
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
@ 2024-12-25 18:38 ` René Scharfe
2024-12-27 10:33 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
2024-12-25 18:38 ` [PATCH 3/4] reftable: handle realloc error in parse_names() René Scharfe
` (3 subsequent siblings)
5 siblings, 2 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-25 18:38 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
When realloc(3) fails, it returns NULL and keeps the original allocation
intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
the allocation count variable in that case, simultaneously leaking the
original allocation and misrepresenting the number of storable items.
parse_names() avoids the leak by keeping the original pointer if
reallocation fails, but still increase the allocation count in such a
case as if it succeeded. That's OK, because the error handling code
just frees everything and doesn't look at names_cap anymore.
reftable_buf_add() does the same, but here it is a problem as it leaves
the reftable_buf in a broken state, with ->alloc being roughly twice as
big as the actually allocated memory, allowing out-of-bounds writes in
subsequent calls.
Reimplement REFTABLE_ALLOC_GROW to avoid leaks, keep allocation counts
in sync and still signal failures to callers while avoiding code
duplication in callers. Make it an expression that evaluates to 0 if no
reallocation is needed or it succeeded and 1 on failure while keeping
the original pointer and allocation counter values.
Adjust REFTABLE_ALLOC_GROW_OR_NULL to the new calling convention for
REFTABLE_ALLOC_GROW, but keep its support for non-size_t alloc variables
for now.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.c | 11 +++--------
reftable/basics.h | 39 ++++++++++++++++++++++++++-------------
2 files changed, 29 insertions(+), 21 deletions(-)
diff --git a/reftable/basics.c b/reftable/basics.c
index 70b1091d14..cd6b39dbe9 100644
--- a/reftable/basics.c
+++ b/reftable/basics.c
@@ -124,11 +124,8 @@ int reftable_buf_add(struct reftable_buf *buf, const void *data, size_t len)
size_t newlen = buf->len + len;
if (newlen + 1 > buf->alloc) {
- char *reallocated = buf->buf;
- REFTABLE_ALLOC_GROW(reallocated, newlen + 1, buf->alloc);
- if (!reallocated)
+ if (REFTABLE_ALLOC_GROW(buf->buf, newlen + 1, buf->alloc))
return REFTABLE_OUT_OF_MEMORY_ERROR;
- buf->buf = reallocated;
}
memcpy(buf->buf + buf->len, data, len);
@@ -233,11 +230,9 @@ char **parse_names(char *buf, int size)
next = end;
}
if (p < next) {
- char **names_grown = names;
- REFTABLE_ALLOC_GROW(names_grown, names_len + 1, names_cap);
- if (!names_grown)
+ if (REFTABLE_ALLOC_GROW(names, names_len + 1,
+ names_cap))
goto err;
- names = names_grown;
names[names_len] = reftable_strdup(p);
if (!names[names_len++])
diff --git a/reftable/basics.h b/reftable/basics.h
index 259f4c274c..fa5d75868b 100644
--- a/reftable/basics.h
+++ b/reftable/basics.h
@@ -120,22 +120,35 @@ char *reftable_strdup(const char *str);
#define REFTABLE_ALLOC_ARRAY(x, alloc) (x) = reftable_malloc(st_mult(sizeof(*(x)), (alloc)))
#define REFTABLE_CALLOC_ARRAY(x, alloc) (x) = reftable_calloc((alloc), sizeof(*(x)))
#define REFTABLE_REALLOC_ARRAY(x, alloc) (x) = reftable_realloc((x), st_mult(sizeof(*(x)), (alloc)))
-#define REFTABLE_ALLOC_GROW(x, nr, alloc) \
- do { \
- if ((nr) > alloc) { \
- alloc = 2 * (alloc) + 1; \
- if (alloc < (nr)) \
- alloc = (nr); \
- REFTABLE_REALLOC_ARRAY(x, alloc); \
- } \
- } while (0)
+
+static inline void *reftable_alloc_grow(void *p, size_t nelem, size_t elsize,
+ size_t *allocp)
+{
+ void *new_p;
+ size_t alloc = *allocp * 2 + 1;
+ if (alloc < nelem)
+ alloc = nelem;
+ new_p = reftable_realloc(p, st_mult(elsize, alloc));
+ if (!new_p)
+ return p;
+ *allocp = alloc;
+ return new_p;
+}
+
+#define REFTABLE_ALLOC_GROW(x, nr, alloc) ( \
+ (nr) > (alloc) && ( \
+ (x) = reftable_alloc_grow((x), (nr), sizeof(*(x)), &(alloc)), \
+ (nr) > (alloc) \
+ ) \
+)
#define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
- void *reftable_alloc_grow_or_null_orig_ptr = (x); \
- REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
- if (!(x)) { \
- reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
+ size_t reftable_alloc_grow_or_null_alloc = alloc; \
+ if (REFTABLE_ALLOC_GROW((x), (nr), reftable_alloc_grow_or_null_alloc)) { \
+ reftable_free(x); \
alloc = 0; \
+ } else { \
+ alloc = reftable_alloc_grow_or_null_alloc; \
} \
} while (0)
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH 3/4] reftable: handle realloc error in parse_names()
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
2024-12-25 18:38 ` [PATCH 2/4] reftable: fix allocation count " René Scharfe
@ 2024-12-25 18:38 ` René Scharfe
2024-12-25 18:38 ` [PATCH 4/4] t-reftable-merged: check realloc errors René Scharfe
` (2 subsequent siblings)
5 siblings, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-25 18:38 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
Check the final reallocation for adding the terminating NULL and handle
it just like those in the loop. Simply use REFTABLE_ALLOC_GROW instead
of keeping the REFTABLE_REALLOC_ARRAY call and adding code to preserve
the original pointer value around it.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/reftable/basics.c b/reftable/basics.c
index cd6b39dbe9..fe2b83ff83 100644
--- a/reftable/basics.c
+++ b/reftable/basics.c
@@ -241,7 +241,8 @@ char **parse_names(char *buf, int size)
p = next + 1;
}
- REFTABLE_REALLOC_ARRAY(names, names_len + 1);
+ if (REFTABLE_ALLOC_GROW(names, names_len + 1, names_cap))
+ goto err;
names[names_len] = NULL;
return names;
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH 4/4] t-reftable-merged: check realloc errors
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
` (2 preceding siblings ...)
2024-12-25 18:38 ` [PATCH 3/4] reftable: handle realloc error in parse_names() René Scharfe
@ 2024-12-25 18:38 ` René Scharfe
2024-12-27 5:46 ` Junio C Hamano
2024-12-27 10:34 ` [PATCH 0/4] reftable: fix realloc error handling Patrick Steinhardt
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
5 siblings, 1 reply; 25+ messages in thread
From: René Scharfe @ 2024-12-25 18:38 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
Report reallocation errors in unit tests, like everywhere else.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
t/unit-tests/t-reftable-merged.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/t/unit-tests/t-reftable-merged.c b/t/unit-tests/t-reftable-merged.c
index a12bd0e1a3..60836f80d6 100644
--- a/t/unit-tests/t-reftable-merged.c
+++ b/t/unit-tests/t-reftable-merged.c
@@ -178,7 +178,7 @@ static void t_merged_refs(void)
if (err > 0)
break;
- REFTABLE_ALLOC_GROW(out, len + 1, cap);
+ check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
out[len++] = ref;
}
reftable_iterator_destroy(&it);
@@ -459,7 +459,7 @@ static void t_merged_logs(void)
if (err > 0)
break;
- REFTABLE_ALLOC_GROW(out, len + 1, cap);
+ check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
out[len++] = log;
}
reftable_iterator_destroy(&it);
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* Re: [PATCH 1/4] reftable: avoid leaks on realloc error
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
@ 2024-12-27 5:39 ` Junio C Hamano
2024-12-27 10:33 ` Patrick Steinhardt
1 sibling, 0 replies; 25+ messages in thread
From: Junio C Hamano @ 2024-12-27 5:39 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List, Patrick Steinhardt
René Scharfe <l.s.r@web.de> writes:
> +#define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
> + void *reftable_alloc_grow_or_null_orig_ptr = (x); \
> + REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
> + if (!(x)) { \
> + reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
> + alloc = 0; \
> + } \
> +} while (0)
It is tricky what (x) means at each reference site ;-) but the above
looks good.
> #define REFTABLE_FREE_AND_NULL(p) do { reftable_free(p); (p) = NULL; } while (0)
>
> #ifndef REFTABLE_ALLOW_BANNED_ALLOCATORS
> diff --git a/reftable/block.c b/reftable/block.c
> index 0198078485..9858bbc7c5 100644
> --- a/reftable/block.c
> +++ b/reftable/block.c
> @@ -53,7 +53,8 @@ static int block_writer_register_restart(struct block_writer *w, int n,
> if (2 + 3 * rlen + n > w->block_size - w->next)
> return -1;
> if (is_restart) {
> - REFTABLE_ALLOC_GROW(w->restarts, w->restart_len + 1, w->restart_cap);
> + REFTABLE_ALLOC_GROW_OR_NULL(w->restarts, w->restart_len + 1,
> + w->restart_cap);
> if (!w->restarts)
> return REFTABLE_OUT_OF_MEMORY_ERROR;
> w->restarts[w->restart_len++] = w->next;
> @@ -176,7 +177,8 @@ int block_writer_finish(struct block_writer *w)
> * is guaranteed to return `Z_STREAM_END`.
> */
> compressed_len = deflateBound(w->zstream, src_len);
> - REFTABLE_ALLOC_GROW(w->compressed, compressed_len, w->compressed_cap);
> + REFTABLE_ALLOC_GROW_OR_NULL(w->compressed, compressed_len,
> + w->compressed_cap);
> if (!w->compressed) {
> ret = REFTABLE_OUT_OF_MEMORY_ERROR;
> return ret;
> @@ -235,8 +237,8 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
> uLong src_len = block->len - block_header_skip;
>
> /* Log blocks specify the *uncompressed* size in their header. */
> - REFTABLE_ALLOC_GROW(br->uncompressed_data, sz,
> - br->uncompressed_cap);
> + REFTABLE_ALLOC_GROW_OR_NULL(br->uncompressed_data, sz,
> + br->uncompressed_cap);
> if (!br->uncompressed_data) {
> err = REFTABLE_OUT_OF_MEMORY_ERROR;
> goto done;
These all have "has the preceding realloc() return NULL?" check and
error handling, so they are strict improvement whose only effect is
to plug the leak (and clearing of the allocation).
> diff --git a/reftable/pq.c b/reftable/pq.c
> index 6ee1164dd3..5591e875e1 100644
> --- a/reftable/pq.c
> +++ b/reftable/pq.c
> @@ -49,7 +49,7 @@ int merged_iter_pqueue_add(struct merged_iter_pqueue *pq, const struct pq_entry
> {
> size_t i = 0;
>
> - REFTABLE_ALLOC_GROW(pq->heap, pq->len + 1, pq->cap);
> + REFTABLE_ALLOC_GROW_OR_NULL(pq->heap, pq->len + 1, pq->cap);
> if (!pq->heap)
> return REFTABLE_OUT_OF_MEMORY_ERROR;
> pq->heap[pq->len++] = *e;
Ditto. And the same can be said to all hunks that follow (omitted).
Makes one wonder what remaining callers of REFTABLE_ALLOC_GROW()
(other than the one in REFTABLE_ALLOC_GROW_OR_NULL()) are getting;
hopefully the next steps will deal with them?
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 4/4] t-reftable-merged: check realloc errors
2024-12-25 18:38 ` [PATCH 4/4] t-reftable-merged: check realloc errors René Scharfe
@ 2024-12-27 5:46 ` Junio C Hamano
2024-12-27 10:34 ` Patrick Steinhardt
0 siblings, 1 reply; 25+ messages in thread
From: Junio C Hamano @ 2024-12-27 5:46 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List, Patrick Steinhardt
René Scharfe <l.s.r@web.de> writes:
> Report reallocation errors in unit tests, like everywhere else.
OK. That's good for consistency if anything else.
We have a test framework for doing unit test at such low level, yet
we cannot really write tests that validates that the right thing
happens when a particular realloc() call returns NULL, which feels
somewhat disappointing, but that is not a fault of this series.
Thanks.
>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> t/unit-tests/t-reftable-merged.c | 4 ++--
> 1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/t/unit-tests/t-reftable-merged.c b/t/unit-tests/t-reftable-merged.c
> index a12bd0e1a3..60836f80d6 100644
> --- a/t/unit-tests/t-reftable-merged.c
> +++ b/t/unit-tests/t-reftable-merged.c
> @@ -178,7 +178,7 @@ static void t_merged_refs(void)
> if (err > 0)
> break;
>
> - REFTABLE_ALLOC_GROW(out, len + 1, cap);
> + check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
> out[len++] = ref;
> }
> reftable_iterator_destroy(&it);
> @@ -459,7 +459,7 @@ static void t_merged_logs(void)
> if (err > 0)
> break;
>
> - REFTABLE_ALLOC_GROW(out, len + 1, cap);
> + check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
> out[len++] = log;
> }
> reftable_iterator_destroy(&it);
> --
> 2.47.1
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 1/4] reftable: avoid leaks on realloc error
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
2024-12-27 5:39 ` Junio C Hamano
@ 2024-12-27 10:33 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
1 sibling, 1 reply; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-27 10:33 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List
On Wed, Dec 25, 2024 at 07:38:29PM +0100, René Scharfe wrote:
> When realloc(3) fails, it returns NULL and keeps the original allocation
> intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
> the allocation count variable in that case, simultaneously leaking the
> original allocation and misrepresenting the number of storable items.
>
> parse_names() and reftable_buf_add() avoid leaking by restoring the
> original pointer value on failure, but all other callers seem to be OK
> with losing the old allocation. Add a new variant of the macro,
> REFTABLE_ALLOC_GROW_OR_NULL, which plugs the leak and zeros the
> allocation counter. Use it for those callers.
Hm, okay. I find it a bit curious to discern those two macros from each
other as all callers need to handle OOM errors anyway, so doing the safe
thing should likely be our default here and all callsites that don't
should be adapted, shouldn't they?
In the case of `reftable_buf_add()` I kind of doubt the usefulness of
handling the error just to keep the old pointer intact, as all callsites
will ultimately error out anyway. But in the case of `parse_names()` we
do in fact want to handle the case specially so that we can free any
names we have already parsed, so that case makes sense indeed.
So there is merit in having two separate wrappers, but it would be nice
if `REFTABLE_ALLOC_GROW()` would be doing the "right thing" for most
cases while the above two callsites would be adapted to use a wrapper
that requires a bit more thought to use correctly. For example something
like `REFTABLE_TRY_ALLOC_GROW()` or similar.
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 2/4] reftable: fix allocation count on realloc error
2024-12-25 18:38 ` [PATCH 2/4] reftable: fix allocation count " René Scharfe
@ 2024-12-27 10:33 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
2024-12-27 20:16 ` René Scharfe
1 sibling, 1 reply; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-27 10:33 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List
On Wed, Dec 25, 2024 at 07:38:35PM +0100, René Scharfe wrote:
> When realloc(3) fails, it returns NULL and keeps the original allocation
> intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
> the allocation count variable in that case, simultaneously leaking the
> original allocation and misrepresenting the number of storable items.
>
> parse_names() avoids the leak by keeping the original pointer if
> reallocation fails, but still increase the allocation count in such a
> case as if it succeeded. That's OK, because the error handling code
> just frees everything and doesn't look at names_cap anymore.
>
> reftable_buf_add() does the same, but here it is a problem as it leaves
> the reftable_buf in a broken state, with ->alloc being roughly twice as
> big as the actually allocated memory, allowing out-of-bounds writes in
> subsequent calls.
>
> Reimplement REFTABLE_ALLOC_GROW to avoid leaks, keep allocation counts
> in sync and still signal failures to callers while avoiding code
> duplication in callers. Make it an expression that evaluates to 0 if no
> reallocation is needed or it succeeded and 1 on failure while keeping
> the original pointer and allocation counter values.
>
> Adjust REFTABLE_ALLOC_GROW_OR_NULL to the new calling convention for
> REFTABLE_ALLOC_GROW, but keep its support for non-size_t alloc variables
> for now.
Okay, this change goes into the direction I was wondering about with the
preceding commit, good.
> diff --git a/reftable/basics.h b/reftable/basics.h
> index 259f4c274c..fa5d75868b 100644
> --- a/reftable/basics.h
> +++ b/reftable/basics.h
> @@ -120,22 +120,35 @@ char *reftable_strdup(const char *str);
> #define REFTABLE_ALLOC_ARRAY(x, alloc) (x) = reftable_malloc(st_mult(sizeof(*(x)), (alloc)))
> #define REFTABLE_CALLOC_ARRAY(x, alloc) (x) = reftable_calloc((alloc), sizeof(*(x)))
> #define REFTABLE_REALLOC_ARRAY(x, alloc) (x) = reftable_realloc((x), st_mult(sizeof(*(x)), (alloc)))
> -#define REFTABLE_ALLOC_GROW(x, nr, alloc) \
> - do { \
> - if ((nr) > alloc) { \
> - alloc = 2 * (alloc) + 1; \
> - if (alloc < (nr)) \
> - alloc = (nr); \
> - REFTABLE_REALLOC_ARRAY(x, alloc); \
> - } \
> - } while (0)
> +
> +static inline void *reftable_alloc_grow(void *p, size_t nelem, size_t elsize,
> + size_t *allocp)
> +{
> + void *new_p;
> + size_t alloc = *allocp * 2 + 1;
> + if (alloc < nelem)
> + alloc = nelem;
> + new_p = reftable_realloc(p, st_mult(elsize, alloc));
> + if (!new_p)
> + return p;
> + *allocp = alloc;
> + return new_p;
> +}
> +
> +#define REFTABLE_ALLOC_GROW(x, nr, alloc) ( \
> + (nr) > (alloc) && ( \
> + (x) = reftable_alloc_grow((x), (nr), sizeof(*(x)), &(alloc)), \
> + (nr) > (alloc) \
> + ) \
> +)
Do we even need this macro? I don't really think it serves much of a
purpose anymore.
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 4/4] t-reftable-merged: check realloc errors
2024-12-27 5:46 ` Junio C Hamano
@ 2024-12-27 10:34 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
0 siblings, 1 reply; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-27 10:34 UTC (permalink / raw)
To: Junio C Hamano; +Cc: René Scharfe, Git List
On Thu, Dec 26, 2024 at 09:46:51PM -0800, Junio C Hamano wrote:
> René Scharfe <l.s.r@web.de> writes:
>
> > Report reallocation errors in unit tests, like everywhere else.
>
> OK. That's good for consistency if anything else.
>
> We have a test framework for doing unit test at such low level, yet
> we cannot really write tests that validates that the right thing
> happens when a particular realloc() call returns NULL, which feels
> somewhat disappointing, but that is not a fault of this series.
In the context of the reftable library we can because we've got
pluggable allocators. We could in theory swap them out against variants
that fail, but it's not easy to make them fail in one specific code
path. For the case at hand though it would work alright.
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 0/4] reftable: fix realloc error handling
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
` (3 preceding siblings ...)
2024-12-25 18:38 ` [PATCH 4/4] t-reftable-merged: check realloc errors René Scharfe
@ 2024-12-27 10:34 ` Patrick Steinhardt
2024-12-27 16:02 ` Junio C Hamano
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
5 siblings, 1 reply; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-27 10:34 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List
On Wed, Dec 25, 2024 at 07:33:07PM +0100, René Scharfe wrote:
> The current handling of reallocation errors leaks the original
> allocation in most cases and corrupts the capacity variable. Fix
> that in REFTABLE_ALLOC_GROW and by providing a new macro
> REFTABLE_ALLOC_GROW_OR_NULL -- solve this somewhat tricky issue
> centrally, with minimal impact to calling code.
>
> And the last two patches add error handling to the remaining
> places that still lack it.
Thanks a lot for working on this!
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 0/4] reftable: fix realloc error handling
2024-12-27 10:34 ` [PATCH 0/4] reftable: fix realloc error handling Patrick Steinhardt
@ 2024-12-27 16:02 ` Junio C Hamano
0 siblings, 0 replies; 25+ messages in thread
From: Junio C Hamano @ 2024-12-27 16:02 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: René Scharfe, Git List
Patrick Steinhardt <ps@pks.im> writes:
> On Wed, Dec 25, 2024 at 07:33:07PM +0100, René Scharfe wrote:
>> The current handling of reallocation errors leaks the original
>> allocation in most cases and corrupts the capacity variable. Fix
>> that in REFTABLE_ALLOC_GROW and by providing a new macro
>> REFTABLE_ALLOC_GROW_OR_NULL -- solve this somewhat tricky issue
>> centrally, with minimal impact to calling code.
>>
>> And the last two patches add error handling to the remaining
>> places that still lack it.
>
> Thanks a lot for working on this!
And thank you for reviewing.
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 2/4] reftable: fix allocation count on realloc error
2024-12-25 18:38 ` [PATCH 2/4] reftable: fix allocation count " René Scharfe
2024-12-27 10:33 ` Patrick Steinhardt
@ 2024-12-27 20:16 ` René Scharfe
1 sibling, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-27 20:16 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt
Am 25.12.24 um 19:38 schrieb René Scharfe:
> When realloc(3) fails, it returns NULL and keeps the original allocation
> intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
> the allocation count variable in that case, simultaneously leaking the
> original allocation and misrepresenting the number of storable items.
>
> parse_names() avoids the leak by keeping the original pointer if
> reallocation fails, but still increase the allocation count in such a
> case as if it succeeded. That's OK, because the error handling code
> just frees everything and doesn't look at names_cap anymore.
>
> reftable_buf_add() does the same, but here it is a problem as it leaves
> the reftable_buf in a broken state, with ->alloc being roughly twice as
> big as the actually allocated memory, allowing out-of-bounds writes in
> subsequent calls.
>
> Reimplement REFTABLE_ALLOC_GROW to avoid leaks, keep allocation counts
> in sync and still signal failures to callers while avoiding code
> duplication in callers. Make it an expression that evaluates to 0 if no
> reallocation is needed or it succeeded and 1 on failure while keeping
> the original pointer and allocation counter values.
>
> Adjust REFTABLE_ALLOC_GROW_OR_NULL to the new calling convention for
> REFTABLE_ALLOC_GROW, but keep its support for non-size_t alloc variables
> for now.
>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> reftable/basics.c | 11 +++--------
> reftable/basics.h | 39 ++++++++++++++++++++++++++-------------
> 2 files changed, 29 insertions(+), 21 deletions(-)
>
> diff --git a/reftable/basics.c b/reftable/basics.c
> index 70b1091d14..cd6b39dbe9 100644
> --- a/reftable/basics.c
> +++ b/reftable/basics.c
> @@ -124,11 +124,8 @@ int reftable_buf_add(struct reftable_buf *buf, const void *data, size_t len)
> size_t newlen = buf->len + len;
>
> if (newlen + 1 > buf->alloc) {
> - char *reallocated = buf->buf;
> - REFTABLE_ALLOC_GROW(reallocated, newlen + 1, buf->alloc);
> - if (!reallocated)
> + if (REFTABLE_ALLOC_GROW(buf->buf, newlen + 1, buf->alloc))
> return REFTABLE_OUT_OF_MEMORY_ERROR;
> - buf->buf = reallocated;
> }
>
> memcpy(buf->buf + buf->len, data, len);
> @@ -233,11 +230,9 @@ char **parse_names(char *buf, int size)
> next = end;
> }
> if (p < next) {
> - char **names_grown = names;
> - REFTABLE_ALLOC_GROW(names_grown, names_len + 1, names_cap);
> - if (!names_grown)
> + if (REFTABLE_ALLOC_GROW(names, names_len + 1,
> + names_cap))
> goto err;
> - names = names_grown;
>
> names[names_len] = reftable_strdup(p);
> if (!names[names_len++])
> diff --git a/reftable/basics.h b/reftable/basics.h
> index 259f4c274c..fa5d75868b 100644
> --- a/reftable/basics.h
> +++ b/reftable/basics.h
> @@ -120,22 +120,35 @@ char *reftable_strdup(const char *str);
> #define REFTABLE_ALLOC_ARRAY(x, alloc) (x) = reftable_malloc(st_mult(sizeof(*(x)), (alloc)))
> #define REFTABLE_CALLOC_ARRAY(x, alloc) (x) = reftable_calloc((alloc), sizeof(*(x)))
> #define REFTABLE_REALLOC_ARRAY(x, alloc) (x) = reftable_realloc((x), st_mult(sizeof(*(x)), (alloc)))
> -#define REFTABLE_ALLOC_GROW(x, nr, alloc) \
> - do { \
> - if ((nr) > alloc) { \
> - alloc = 2 * (alloc) + 1; \
> - if (alloc < (nr)) \
> - alloc = (nr); \
> - REFTABLE_REALLOC_ARRAY(x, alloc); \
> - } \
> - } while (0)
> +
> +static inline void *reftable_alloc_grow(void *p, size_t nelem, size_t elsize,
> + size_t *allocp)
> +{
> + void *new_p;
> + size_t alloc = *allocp * 2 + 1;
> + if (alloc < nelem)
> + alloc = nelem;
> + new_p = reftable_realloc(p, st_mult(elsize, alloc));
> + if (!new_p)
> + return p;
> + *allocp = alloc;
> + return new_p;
> +}
> +
> +#define REFTABLE_ALLOC_GROW(x, nr, alloc) ( \
> + (nr) > (alloc) && ( \
> + (x) = reftable_alloc_grow((x), (nr), sizeof(*(x)), &(alloc)), \
> + (nr) > (alloc) \
> + ) \
> +)
>
> #define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
> - void *reftable_alloc_grow_or_null_orig_ptr = (x); \
> - REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
> - if (!(x)) { \
> - reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
> + size_t reftable_alloc_grow_or_null_alloc = alloc; \
> + if (REFTABLE_ALLOC_GROW((x), (nr), reftable_alloc_grow_or_null_alloc)) { \
> + reftable_free(x); \
> alloc = 0; \
This forgets to set x = NULL, which is bad since that's what callers check. :-O
It was not necessary without this patch, because realloc(3) did it.
> + } else { \
> + alloc = reftable_alloc_grow_or_null_alloc; \
> } \
> } while (0)
>
> --
> 2.47.1
>
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 4/4] t-reftable-merged: check realloc errors
2024-12-27 10:34 ` Patrick Steinhardt
@ 2024-12-27 20:16 ` René Scharfe
0 siblings, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-27 20:16 UTC (permalink / raw)
To: Patrick Steinhardt, Junio C Hamano; +Cc: Git List
Am 27.12.24 um 11:34 schrieb Patrick Steinhardt:
> On Thu, Dec 26, 2024 at 09:46:51PM -0800, Junio C Hamano wrote:
>> René Scharfe <l.s.r@web.de> writes:
>>
>>> Report reallocation errors in unit tests, like everywhere else.
>>
>> OK. That's good for consistency if anything else.
>>
>> We have a test framework for doing unit test at such low level, yet
>> we cannot really write tests that validates that the right thing
>> happens when a particular realloc() call returns NULL, which feels
>> somewhat disappointing, but that is not a fault of this series.
>
> In the context of the reftable library we can because we've got
> pluggable allocators. We could in theory swap them out against variants
> that fail, but it's not easy to make them fail in one specific code
> path. For the case at hand though it would work alright.
It should be easy and safe to provide allocator stubs that just fail
and swap them in and out with reftable_set_alloc() before and after
an operation that performs a single allocation. Will add basic
tests in the next round.
Injecting an allocation error in the middle of parse_names() would
require a version that starts failing after a certain number of
allocations (or allocated bytes). Possible, but not in scope for
this series.
René
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 2/4] reftable: fix allocation count on realloc error
2024-12-27 10:33 ` Patrick Steinhardt
@ 2024-12-27 20:16 ` René Scharfe
0 siblings, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-27 20:16 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Git List
Am 27.12.24 um 11:33 schrieb Patrick Steinhardt:
> On Wed, Dec 25, 2024 at 07:38:35PM +0100, René Scharfe wrote:
>
>> diff --git a/reftable/basics.h b/reftable/basics.h
>> index 259f4c274c..fa5d75868b 100644
>> --- a/reftable/basics.h
>> +++ b/reftable/basics.h
>> @@ -120,22 +120,35 @@ char *reftable_strdup(const char *str);
>> #define REFTABLE_ALLOC_ARRAY(x, alloc) (x) = reftable_malloc(st_mult(sizeof(*(x)), (alloc)))
>> #define REFTABLE_CALLOC_ARRAY(x, alloc) (x) = reftable_calloc((alloc), sizeof(*(x)))
>> #define REFTABLE_REALLOC_ARRAY(x, alloc) (x) = reftable_realloc((x), st_mult(sizeof(*(x)), (alloc)))
>> +#define REFTABLE_ALLOC_GROW(x, nr, alloc) ( \
>> + (nr) > (alloc) && ( \
>> + (x) = reftable_alloc_grow((x), (nr), sizeof(*(x)), &(alloc)), \
>> + (nr) > (alloc) \
>> + ) \
>> +)
>
> Do we even need this macro? I don't really think it serves much of a
> purpose anymore.
It provides the same value as the *ALLOC_ARRAY macros above: automatic
sizeof handling. Plus it returns whether the allocation succeeded,
avoiding repetition of its arguments in callers.
René
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH 1/4] reftable: avoid leaks on realloc error
2024-12-27 10:33 ` Patrick Steinhardt
@ 2024-12-27 20:16 ` René Scharfe
2024-12-30 6:29 ` Patrick Steinhardt
0 siblings, 1 reply; 25+ messages in thread
From: René Scharfe @ 2024-12-27 20:16 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Git List
Am 27.12.24 um 11:33 schrieb Patrick Steinhardt:
> On Wed, Dec 25, 2024 at 07:38:29PM +0100, René Scharfe wrote:
>> When realloc(3) fails, it returns NULL and keeps the original allocation
>> intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
>> the allocation count variable in that case, simultaneously leaking the
>> original allocation and misrepresenting the number of storable items.
>>
>> parse_names() and reftable_buf_add() avoid leaking by restoring the
>> original pointer value on failure, but all other callers seem to be OK
>> with losing the old allocation. Add a new variant of the macro,
>> REFTABLE_ALLOC_GROW_OR_NULL, which plugs the leak and zeros the
>> allocation counter. Use it for those callers.
>
> Hm, okay. I find it a bit curious to discern those two macros from each
> other as all callers need to handle OOM errors anyway, so doing the safe
> thing should likely be our default here and all callsites that don't
> should be adapted, shouldn't they?
I agree, and I my first version only had REFTABLE_ALLOC_GROW. Keeping
stuff unchanged if we cannot grow should be safer, right? But it would
introduce a leak if the caller exits without cleaning up, so each of
them needs to be audited. I was too lazy for that. And it's work that
can be parallelized..
> In the case of `reftable_buf_add()` I kind of doubt the usefulness of
> handling the error just to keep the old pointer intact, as all callsites
> will ultimately error out anyway.
I can imagine use cases where an object is built piece by piece, one
part is too large and then you still want to keep all the rest and just
replace the huge thing with a placeholder or entirely ignore it. Could
be a case of YAGNI, though.
> But in the case of `parse_names()` we
> do in fact want to handle the case specially so that we can free any
> names we have already parsed, so that case makes sense indeed.
Yes. But that leads me on a tangent: Is it really a good idea to load
a file into lots of individual string objects instead of loading into
a single big buffer and pointing directly into it? Do those strings
need to have individual lifetimes?
> So there is merit in having two separate wrappers, but it would be nice
> if `REFTABLE_ALLOC_GROW()` would be doing the "right thing" for most
> cases while the above two callsites would be adapted to use a wrapper
> that requires a bit more thought to use correctly. For example something
> like `REFTABLE_TRY_ALLOC_GROW()` or similar.
So this is about naming? And with "right thing" you mean failing to
grow should lead to destruction?
René
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v2 0/4] reftable: fix realloc error handling
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
` (4 preceding siblings ...)
2024-12-27 10:34 ` [PATCH 0/4] reftable: fix realloc error handling Patrick Steinhardt
@ 2024-12-28 9:43 ` René Scharfe
2024-12-28 9:47 ` [PATCH v2 1/4] reftable: avoid leaks on realloc error René Scharfe
` (4 more replies)
5 siblings, 5 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-28 9:43 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt, Junio C Hamano
Changes since v1:
- added unit tests
- explicitly set pointer to NULL on REFTABLE_ALLOC_GROW_OR_NULL failure
in patch 2; omission found by unit test
reftable: avoid leaks on realloc error
reftable: fix allocation count on realloc error
reftable: handle realloc error in parse_names()
t-reftable-merged: handle realloc errors
reftable/basics.c | 14 +++-----
reftable/basics.h | 41 ++++++++++++++++++-----
reftable/block.c | 10 +++---
reftable/pq.c | 2 +-
reftable/record.c | 12 +++----
reftable/stack.c | 8 +++--
reftable/writer.c | 5 +--
t/unit-tests/t-reftable-basics.c | 56 ++++++++++++++++++++++++++++++++
t/unit-tests/t-reftable-merged.c | 4 +--
9 files changed, 116 insertions(+), 36 deletions(-)
Range-diff against v1:
1: b41547720d ! 1: b3cad92038 reftable: avoid leaks on realloc error
@@ reftable/writer.c: static int writer_flush_nonempty_block(struct reftable_writer
if (!w->index)
return REFTABLE_OUT_OF_MEMORY_ERROR;
+
+ ## t/unit-tests/t-reftable-basics.c ##
+@@ t/unit-tests/t-reftable-basics.c: static int integer_needle_lesseq(size_t i, void *_args)
+ return args->needle <= args->haystack[i];
+ }
+
++static void *realloc_stub(void *p UNUSED, size_t size UNUSED)
++{
++ return NULL;
++}
++
+ int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
+ {
+ if_test ("binary search with binsearch works") {
+@@ t/unit-tests/t-reftable-basics.c: int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
+ check_int(in, ==, out);
+ }
+
++ if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
++ int *arr = NULL;
++ size_t alloc = 0, old_alloc;
++
++ REFTABLE_ALLOC_GROW_OR_NULL(arr, 1, alloc);
++ check(arr != NULL);
++ check_uint(alloc, >=, 1);
++ arr[0] = 42;
++
++ old_alloc = alloc;
++ REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
++ check(arr != NULL);
++ check_uint(alloc, >, old_alloc);
++ arr[alloc - 1] = 42;
++
++ old_alloc = alloc;
++ reftable_set_alloc(malloc, realloc_stub, free);
++ REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
++ check(arr == NULL);
++ check_uint(alloc, ==, 0);
++ reftable_set_alloc(malloc, realloc, free);
++
++ reftable_free(arr);
++ }
++
+ return test_done();
+ }
2: bde2f0e4a5 ! 2: 62a1042825 reftable: fix allocation count on realloc error
@@ reftable/basics.h: char *reftable_strdup(const char *str);
- reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
+ size_t reftable_alloc_grow_or_null_alloc = alloc; \
+ if (REFTABLE_ALLOC_GROW((x), (nr), reftable_alloc_grow_or_null_alloc)) { \
-+ reftable_free(x); \
++ REFTABLE_FREE_AND_NULL(x); \
alloc = 0; \
+ } else { \
+ alloc = reftable_alloc_grow_or_null_alloc; \
} \
} while (0)
+
+ ## t/unit-tests/t-reftable-basics.c ##
+@@ t/unit-tests/t-reftable-basics.c: int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
+ check_int(in, ==, out);
+ }
+
++ if_test ("REFTABLE_ALLOC_GROW works") {
++ int *arr = NULL, *old_arr;
++ size_t alloc = 0, old_alloc;
++
++ check(!REFTABLE_ALLOC_GROW(arr, 1, alloc));
++ check(arr != NULL);
++ check_uint(alloc, >=, 1);
++ arr[0] = 42;
++
++ old_alloc = alloc;
++ old_arr = arr;
++ reftable_set_alloc(malloc, realloc_stub, free);
++ check(REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
++ check(arr == old_arr);
++ check_uint(alloc, ==, old_alloc);
++
++ old_alloc = alloc;
++ reftable_set_alloc(malloc, realloc, free);
++ check(!REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
++ check(arr != NULL);
++ check_uint(alloc, >, old_alloc);
++ arr[alloc - 1] = 42;
++
++ reftable_free(arr);
++ }
++
+ if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
+ int *arr = NULL;
+ size_t alloc = 0, old_alloc;
3: 7c9f044813 = 3: ed1a292622 reftable: handle realloc error in parse_names()
4: 3d9493f48a = 4: 916032657e t-reftable-merged: handle realloc errors
--
2.47.1
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v2 1/4] reftable: avoid leaks on realloc error
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
@ 2024-12-28 9:47 ` René Scharfe
2024-12-30 7:25 ` Patrick Steinhardt
2024-12-28 9:48 ` [PATCH v2 2/4] reftable: fix allocation count " René Scharfe
` (3 subsequent siblings)
4 siblings, 1 reply; 25+ messages in thread
From: René Scharfe @ 2024-12-28 9:47 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt, Junio C Hamano
When realloc(3) fails, it returns NULL and keeps the original allocation
intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
the allocation count variable in that case, simultaneously leaking the
original allocation and misrepresenting the number of storable items.
parse_names() and reftable_buf_add() avoid leaking by restoring the
original pointer value on failure, but all other callers seem to be OK
with losing the old allocation. Add a new variant of the macro,
REFTABLE_ALLOC_GROW_OR_NULL, which plugs the leak and zeros the
allocation counter. Use it for those callers.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.h | 10 ++++++++++
reftable/block.c | 10 ++++++----
reftable/pq.c | 2 +-
reftable/record.c | 12 ++++++------
reftable/stack.c | 8 +++++---
reftable/writer.c | 5 +++--
t/unit-tests/t-reftable-basics.c | 30 ++++++++++++++++++++++++++++++
7 files changed, 61 insertions(+), 16 deletions(-)
diff --git a/reftable/basics.h b/reftable/basics.h
index 36beda2c25..259f4c274c 100644
--- a/reftable/basics.h
+++ b/reftable/basics.h
@@ -129,6 +129,16 @@ char *reftable_strdup(const char *str);
REFTABLE_REALLOC_ARRAY(x, alloc); \
} \
} while (0)
+
+#define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
+ void *reftable_alloc_grow_or_null_orig_ptr = (x); \
+ REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
+ if (!(x)) { \
+ reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
+ alloc = 0; \
+ } \
+} while (0)
+
#define REFTABLE_FREE_AND_NULL(p) do { reftable_free(p); (p) = NULL; } while (0)
#ifndef REFTABLE_ALLOW_BANNED_ALLOCATORS
diff --git a/reftable/block.c b/reftable/block.c
index 0198078485..9858bbc7c5 100644
--- a/reftable/block.c
+++ b/reftable/block.c
@@ -53,7 +53,8 @@ static int block_writer_register_restart(struct block_writer *w, int n,
if (2 + 3 * rlen + n > w->block_size - w->next)
return -1;
if (is_restart) {
- REFTABLE_ALLOC_GROW(w->restarts, w->restart_len + 1, w->restart_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->restarts, w->restart_len + 1,
+ w->restart_cap);
if (!w->restarts)
return REFTABLE_OUT_OF_MEMORY_ERROR;
w->restarts[w->restart_len++] = w->next;
@@ -176,7 +177,8 @@ int block_writer_finish(struct block_writer *w)
* is guaranteed to return `Z_STREAM_END`.
*/
compressed_len = deflateBound(w->zstream, src_len);
- REFTABLE_ALLOC_GROW(w->compressed, compressed_len, w->compressed_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->compressed, compressed_len,
+ w->compressed_cap);
if (!w->compressed) {
ret = REFTABLE_OUT_OF_MEMORY_ERROR;
return ret;
@@ -235,8 +237,8 @@ int block_reader_init(struct block_reader *br, struct reftable_block *block,
uLong src_len = block->len - block_header_skip;
/* Log blocks specify the *uncompressed* size in their header. */
- REFTABLE_ALLOC_GROW(br->uncompressed_data, sz,
- br->uncompressed_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(br->uncompressed_data, sz,
+ br->uncompressed_cap);
if (!br->uncompressed_data) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/pq.c b/reftable/pq.c
index 6ee1164dd3..5591e875e1 100644
--- a/reftable/pq.c
+++ b/reftable/pq.c
@@ -49,7 +49,7 @@ int merged_iter_pqueue_add(struct merged_iter_pqueue *pq, const struct pq_entry
{
size_t i = 0;
- REFTABLE_ALLOC_GROW(pq->heap, pq->len + 1, pq->cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(pq->heap, pq->len + 1, pq->cap);
if (!pq->heap)
return REFTABLE_OUT_OF_MEMORY_ERROR;
pq->heap[pq->len++] = *e;
diff --git a/reftable/record.c b/reftable/record.c
index fb5652ed57..04429d23fe 100644
--- a/reftable/record.c
+++ b/reftable/record.c
@@ -246,8 +246,8 @@ static int reftable_ref_record_copy_from(void *rec, const void *src_rec,
if (src->refname) {
size_t refname_len = strlen(src->refname);
- REFTABLE_ALLOC_GROW(ref->refname, refname_len + 1,
- ref->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(ref->refname, refname_len + 1,
+ ref->refname_cap);
if (!ref->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto out;
@@ -385,7 +385,7 @@ static int reftable_ref_record_decode(void *rec, struct reftable_buf key,
SWAP(r->refname, refname);
SWAP(r->refname_cap, refname_cap);
- REFTABLE_ALLOC_GROW(r->refname, key.len + 1, r->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->refname, key.len + 1, r->refname_cap);
if (!r->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -839,7 +839,7 @@ static int reftable_log_record_decode(void *rec, struct reftable_buf key,
if (key.len <= 9 || key.buf[key.len - 9] != 0)
return REFTABLE_FORMAT_ERROR;
- REFTABLE_ALLOC_GROW(r->refname, key.len - 8, r->refname_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->refname, key.len - 8, r->refname_cap);
if (!r->refname) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -947,8 +947,8 @@ static int reftable_log_record_decode(void *rec, struct reftable_buf key,
}
string_view_consume(&in, n);
- REFTABLE_ALLOC_GROW(r->value.update.message, scratch->len + 1,
- r->value.update.message_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(r->value.update.message, scratch->len + 1,
+ r->value.update.message_cap);
if (!r->value.update.message) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/stack.c b/reftable/stack.c
index 634f0c5425..531660a49f 100644
--- a/reftable/stack.c
+++ b/reftable/stack.c
@@ -317,7 +317,9 @@ static int reftable_stack_reload_once(struct reftable_stack *st,
* thus need to keep them alive here, which we
* do by bumping their refcount.
*/
- REFTABLE_ALLOC_GROW(reused, reused_len + 1, reused_alloc);
+ REFTABLE_ALLOC_GROW_OR_NULL(reused,
+ reused_len + 1,
+ reused_alloc);
if (!reused) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
@@ -949,8 +951,8 @@ int reftable_addition_add(struct reftable_addition *add,
if (err < 0)
goto done;
- REFTABLE_ALLOC_GROW(add->new_tables, add->new_tables_len + 1,
- add->new_tables_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(add->new_tables, add->new_tables_len + 1,
+ add->new_tables_cap);
if (!add->new_tables) {
err = REFTABLE_OUT_OF_MEMORY_ERROR;
goto done;
diff --git a/reftable/writer.c b/reftable/writer.c
index 624e90fb53..740c98038e 100644
--- a/reftable/writer.c
+++ b/reftable/writer.c
@@ -254,7 +254,8 @@ static int writer_index_hash(struct reftable_writer *w, struct reftable_buf *has
if (key->offset_len > 0 && key->offsets[key->offset_len - 1] == off)
return 0;
- REFTABLE_ALLOC_GROW(key->offsets, key->offset_len + 1, key->offset_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(key->offsets, key->offset_len + 1,
+ key->offset_cap);
if (!key->offsets)
return REFTABLE_OUT_OF_MEMORY_ERROR;
key->offsets[key->offset_len++] = off;
@@ -820,7 +821,7 @@ static int writer_flush_nonempty_block(struct reftable_writer *w)
* Note that this also applies when flushing index blocks, in which
* case we will end up with a multi-level index.
*/
- REFTABLE_ALLOC_GROW(w->index, w->index_len + 1, w->index_cap);
+ REFTABLE_ALLOC_GROW_OR_NULL(w->index, w->index_len + 1, w->index_cap);
if (!w->index)
return REFTABLE_OUT_OF_MEMORY_ERROR;
diff --git a/t/unit-tests/t-reftable-basics.c b/t/unit-tests/t-reftable-basics.c
index 65d50df091..5bf79c9976 100644
--- a/t/unit-tests/t-reftable-basics.c
+++ b/t/unit-tests/t-reftable-basics.c
@@ -20,6 +20,11 @@ static int integer_needle_lesseq(size_t i, void *_args)
return args->needle <= args->haystack[i];
}
+static void *realloc_stub(void *p UNUSED, size_t size UNUSED)
+{
+ return NULL;
+}
+
int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
{
if_test ("binary search with binsearch works") {
@@ -141,5 +146,30 @@ int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
check_int(in, ==, out);
}
+ if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
+ int *arr = NULL;
+ size_t alloc = 0, old_alloc;
+
+ REFTABLE_ALLOC_GROW_OR_NULL(arr, 1, alloc);
+ check(arr != NULL);
+ check_uint(alloc, >=, 1);
+ arr[0] = 42;
+
+ old_alloc = alloc;
+ REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
+ check(arr != NULL);
+ check_uint(alloc, >, old_alloc);
+ arr[alloc - 1] = 42;
+
+ old_alloc = alloc;
+ reftable_set_alloc(malloc, realloc_stub, free);
+ REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
+ check(arr == NULL);
+ check_uint(alloc, ==, 0);
+ reftable_set_alloc(malloc, realloc, free);
+
+ reftable_free(arr);
+ }
+
return test_done();
}
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 2/4] reftable: fix allocation count on realloc error
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
2024-12-28 9:47 ` [PATCH v2 1/4] reftable: avoid leaks on realloc error René Scharfe
@ 2024-12-28 9:48 ` René Scharfe
2024-12-28 9:48 ` [PATCH v2 3/4] reftable: handle realloc error in parse_names() René Scharfe
` (2 subsequent siblings)
4 siblings, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-28 9:48 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt, Junio C Hamano
When realloc(3) fails, it returns NULL and keeps the original allocation
intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
the allocation count variable in that case, simultaneously leaking the
original allocation and misrepresenting the number of storable items.
parse_names() avoids the leak by keeping the original pointer if
reallocation fails, but still increase the allocation count in such a
case as if it succeeded. That's OK, because the error handling code
just frees everything and doesn't look at names_cap anymore.
reftable_buf_add() does the same, but here it is a problem as it leaves
the reftable_buf in a broken state, with ->alloc being roughly twice as
big as the actually allocated memory, allowing out-of-bounds writes in
subsequent calls.
Reimplement REFTABLE_ALLOC_GROW to avoid leaks, keep allocation counts
in sync and still signal failures to callers while avoiding code
duplication in callers. Make it an expression that evaluates to 0 if no
reallocation is needed or it succeeded and 1 on failure while keeping
the original pointer and allocation counter values.
Adjust REFTABLE_ALLOC_GROW_OR_NULL to the new calling convention for
REFTABLE_ALLOC_GROW, but keep its support for non-size_t alloc variables
for now.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.c | 11 +++------
reftable/basics.h | 39 +++++++++++++++++++++-----------
t/unit-tests/t-reftable-basics.c | 26 +++++++++++++++++++++
3 files changed, 55 insertions(+), 21 deletions(-)
diff --git a/reftable/basics.c b/reftable/basics.c
index 70b1091d14..cd6b39dbe9 100644
--- a/reftable/basics.c
+++ b/reftable/basics.c
@@ -124,11 +124,8 @@ int reftable_buf_add(struct reftable_buf *buf, const void *data, size_t len)
size_t newlen = buf->len + len;
if (newlen + 1 > buf->alloc) {
- char *reallocated = buf->buf;
- REFTABLE_ALLOC_GROW(reallocated, newlen + 1, buf->alloc);
- if (!reallocated)
+ if (REFTABLE_ALLOC_GROW(buf->buf, newlen + 1, buf->alloc))
return REFTABLE_OUT_OF_MEMORY_ERROR;
- buf->buf = reallocated;
}
memcpy(buf->buf + buf->len, data, len);
@@ -233,11 +230,9 @@ char **parse_names(char *buf, int size)
next = end;
}
if (p < next) {
- char **names_grown = names;
- REFTABLE_ALLOC_GROW(names_grown, names_len + 1, names_cap);
- if (!names_grown)
+ if (REFTABLE_ALLOC_GROW(names, names_len + 1,
+ names_cap))
goto err;
- names = names_grown;
names[names_len] = reftable_strdup(p);
if (!names[names_len++])
diff --git a/reftable/basics.h b/reftable/basics.h
index 259f4c274c..4bf71b0954 100644
--- a/reftable/basics.h
+++ b/reftable/basics.h
@@ -120,22 +120,35 @@ char *reftable_strdup(const char *str);
#define REFTABLE_ALLOC_ARRAY(x, alloc) (x) = reftable_malloc(st_mult(sizeof(*(x)), (alloc)))
#define REFTABLE_CALLOC_ARRAY(x, alloc) (x) = reftable_calloc((alloc), sizeof(*(x)))
#define REFTABLE_REALLOC_ARRAY(x, alloc) (x) = reftable_realloc((x), st_mult(sizeof(*(x)), (alloc)))
-#define REFTABLE_ALLOC_GROW(x, nr, alloc) \
- do { \
- if ((nr) > alloc) { \
- alloc = 2 * (alloc) + 1; \
- if (alloc < (nr)) \
- alloc = (nr); \
- REFTABLE_REALLOC_ARRAY(x, alloc); \
- } \
- } while (0)
+
+static inline void *reftable_alloc_grow(void *p, size_t nelem, size_t elsize,
+ size_t *allocp)
+{
+ void *new_p;
+ size_t alloc = *allocp * 2 + 1;
+ if (alloc < nelem)
+ alloc = nelem;
+ new_p = reftable_realloc(p, st_mult(elsize, alloc));
+ if (!new_p)
+ return p;
+ *allocp = alloc;
+ return new_p;
+}
+
+#define REFTABLE_ALLOC_GROW(x, nr, alloc) ( \
+ (nr) > (alloc) && ( \
+ (x) = reftable_alloc_grow((x), (nr), sizeof(*(x)), &(alloc)), \
+ (nr) > (alloc) \
+ ) \
+)
#define REFTABLE_ALLOC_GROW_OR_NULL(x, nr, alloc) do { \
- void *reftable_alloc_grow_or_null_orig_ptr = (x); \
- REFTABLE_ALLOC_GROW((x), (nr), (alloc)); \
- if (!(x)) { \
- reftable_free(reftable_alloc_grow_or_null_orig_ptr); \
+ size_t reftable_alloc_grow_or_null_alloc = alloc; \
+ if (REFTABLE_ALLOC_GROW((x), (nr), reftable_alloc_grow_or_null_alloc)) { \
+ REFTABLE_FREE_AND_NULL(x); \
alloc = 0; \
+ } else { \
+ alloc = reftable_alloc_grow_or_null_alloc; \
} \
} while (0)
diff --git a/t/unit-tests/t-reftable-basics.c b/t/unit-tests/t-reftable-basics.c
index 5bf79c9976..990dc1a244 100644
--- a/t/unit-tests/t-reftable-basics.c
+++ b/t/unit-tests/t-reftable-basics.c
@@ -146,6 +146,32 @@ int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
check_int(in, ==, out);
}
+ if_test ("REFTABLE_ALLOC_GROW works") {
+ int *arr = NULL, *old_arr;
+ size_t alloc = 0, old_alloc;
+
+ check(!REFTABLE_ALLOC_GROW(arr, 1, alloc));
+ check(arr != NULL);
+ check_uint(alloc, >=, 1);
+ arr[0] = 42;
+
+ old_alloc = alloc;
+ old_arr = arr;
+ reftable_set_alloc(malloc, realloc_stub, free);
+ check(REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
+ check(arr == old_arr);
+ check_uint(alloc, ==, old_alloc);
+
+ old_alloc = alloc;
+ reftable_set_alloc(malloc, realloc, free);
+ check(!REFTABLE_ALLOC_GROW(arr, old_alloc + 1, alloc));
+ check(arr != NULL);
+ check_uint(alloc, >, old_alloc);
+ arr[alloc - 1] = 42;
+
+ reftable_free(arr);
+ }
+
if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
int *arr = NULL;
size_t alloc = 0, old_alloc;
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 3/4] reftable: handle realloc error in parse_names()
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
2024-12-28 9:47 ` [PATCH v2 1/4] reftable: avoid leaks on realloc error René Scharfe
2024-12-28 9:48 ` [PATCH v2 2/4] reftable: fix allocation count " René Scharfe
@ 2024-12-28 9:48 ` René Scharfe
2024-12-30 7:25 ` Patrick Steinhardt
2024-12-28 9:49 ` [PATCH v2 4/4] t-reftable-merged: handle realloc errors René Scharfe
2024-12-30 7:25 ` [PATCH v2 0/4] reftable: fix realloc error handling Patrick Steinhardt
4 siblings, 1 reply; 25+ messages in thread
From: René Scharfe @ 2024-12-28 9:48 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt, Junio C Hamano
Check the final reallocation for adding the terminating NULL and handle
it just like those in the loop. Simply use REFTABLE_ALLOC_GROW instead
of keeping the REFTABLE_REALLOC_ARRAY call and adding code to preserve
the original pointer value around it.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
reftable/basics.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/reftable/basics.c b/reftable/basics.c
index cd6b39dbe9..fe2b83ff83 100644
--- a/reftable/basics.c
+++ b/reftable/basics.c
@@ -241,7 +241,8 @@ char **parse_names(char *buf, int size)
p = next + 1;
}
- REFTABLE_REALLOC_ARRAY(names, names_len + 1);
+ if (REFTABLE_ALLOC_GROW(names, names_len + 1, names_cap))
+ goto err;
names[names_len] = NULL;
return names;
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* [PATCH v2 4/4] t-reftable-merged: handle realloc errors
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
` (2 preceding siblings ...)
2024-12-28 9:48 ` [PATCH v2 3/4] reftable: handle realloc error in parse_names() René Scharfe
@ 2024-12-28 9:49 ` René Scharfe
2024-12-30 7:25 ` [PATCH v2 0/4] reftable: fix realloc error handling Patrick Steinhardt
4 siblings, 0 replies; 25+ messages in thread
From: René Scharfe @ 2024-12-28 9:49 UTC (permalink / raw)
To: Git List; +Cc: Patrick Steinhardt, Junio C Hamano
Check reallocation errors in unit tests, like everywhere else.
Signed-off-by: René Scharfe <l.s.r@web.de>
---
t/unit-tests/t-reftable-merged.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/t/unit-tests/t-reftable-merged.c b/t/unit-tests/t-reftable-merged.c
index a12bd0e1a3..60836f80d6 100644
--- a/t/unit-tests/t-reftable-merged.c
+++ b/t/unit-tests/t-reftable-merged.c
@@ -178,7 +178,7 @@ static void t_merged_refs(void)
if (err > 0)
break;
- REFTABLE_ALLOC_GROW(out, len + 1, cap);
+ check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
out[len++] = ref;
}
reftable_iterator_destroy(&it);
@@ -459,7 +459,7 @@ static void t_merged_logs(void)
if (err > 0)
break;
- REFTABLE_ALLOC_GROW(out, len + 1, cap);
+ check(!REFTABLE_ALLOC_GROW(out, len + 1, cap));
out[len++] = log;
}
reftable_iterator_destroy(&it);
--
2.47.1
^ permalink raw reply related [flat|nested] 25+ messages in thread
* Re: [PATCH 1/4] reftable: avoid leaks on realloc error
2024-12-27 20:16 ` René Scharfe
@ 2024-12-30 6:29 ` Patrick Steinhardt
0 siblings, 0 replies; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-30 6:29 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List
On Fri, Dec 27, 2024 at 09:16:27PM +0100, René Scharfe wrote:
> Am 27.12.24 um 11:33 schrieb Patrick Steinhardt:
> > On Wed, Dec 25, 2024 at 07:38:29PM +0100, René Scharfe wrote:
> >> When realloc(3) fails, it returns NULL and keeps the original allocation
> >> intact. REFTABLE_ALLOC_GROW overwrites both the original pointer and
> >> the allocation count variable in that case, simultaneously leaking the
> >> original allocation and misrepresenting the number of storable items.
> >>
> >> parse_names() and reftable_buf_add() avoid leaking by restoring the
> >> original pointer value on failure, but all other callers seem to be OK
> >> with losing the old allocation. Add a new variant of the macro,
> >> REFTABLE_ALLOC_GROW_OR_NULL, which plugs the leak and zeros the
> >> allocation counter. Use it for those callers.
> >
> > Hm, okay. I find it a bit curious to discern those two macros from each
> > other as all callers need to handle OOM errors anyway, so doing the safe
> > thing should likely be our default here and all callsites that don't
> > should be adapted, shouldn't they?
>
> I agree, and I my first version only had REFTABLE_ALLOC_GROW. Keeping
> stuff unchanged if we cannot grow should be safer, right? But it would
> introduce a leak if the caller exits without cleaning up, so each of
> them needs to be audited. I was too lazy for that. And it's work that
> can be parallelized..
Fair enough.
> > In the case of `reftable_buf_add()` I kind of doubt the usefulness of
> > handling the error just to keep the old pointer intact, as all callsites
> > will ultimately error out anyway.
>
> I can imagine use cases where an object is built piece by piece, one
> part is too large and then you still want to keep all the rest and just
> replace the huge thing with a placeholder or entirely ignore it. Could
> be a case of YAGNI, though.
Probably.
> > But in the case of `parse_names()` we
> > do in fact want to handle the case specially so that we can free any
> > names we have already parsed, so that case makes sense indeed.
>
> Yes. But that leads me on a tangent: Is it really a good idea to load
> a file into lots of individual string objects instead of loading into
> a single big buffer and pointing directly into it? Do those strings
> need to have individual lifetimes?
Good question indeed. I don't think we ever need individual lifetimes
here. On the other hand it's probably okayish, too, given that the
number of table names should be limited due to automatic compaction.
> > So there is merit in having two separate wrappers, but it would be nice
> > if `REFTABLE_ALLOC_GROW()` would be doing the "right thing" for most
> > cases while the above two callsites would be adapted to use a wrapper
> > that requires a bit more thought to use correctly. For example something
> > like `REFTABLE_TRY_ALLOC_GROW()` or similar.
>
> So this is about naming? And with "right thing" you mean failing to
> grow should lead to destruction?
Yup, exactly. I just want to give callers a better indicator which of
these functions does what, and having sane defaults helps in my opinion.
I guess this also depends on whether or not we want to eventually adapt
all callsites to handle allocation errors themselves.
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH v2 1/4] reftable: avoid leaks on realloc error
2024-12-28 9:47 ` [PATCH v2 1/4] reftable: avoid leaks on realloc error René Scharfe
@ 2024-12-30 7:25 ` Patrick Steinhardt
0 siblings, 0 replies; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-30 7:25 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List, Junio C Hamano
On Sat, Dec 28, 2024 at 10:47:05AM +0100, René Scharfe wrote:
> @@ -141,5 +146,30 @@ int cmd_main(int argc UNUSED, const char *argv[] UNUSED)
> check_int(in, ==, out);
> }
>
> + if_test ("REFTABLE_ALLOC_GROW_OR_NULL works") {
> + int *arr = NULL;
> + size_t alloc = 0, old_alloc;
> +
> + REFTABLE_ALLOC_GROW_OR_NULL(arr, 1, alloc);
> + check(arr != NULL);
> + check_uint(alloc, >=, 1);
> + arr[0] = 42;
> +
> + old_alloc = alloc;
> + REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
> + check(arr != NULL);
> + check_uint(alloc, >, old_alloc);
> + arr[alloc - 1] = 42;
> +
> + old_alloc = alloc;
> + reftable_set_alloc(malloc, realloc_stub, free);
> + REFTABLE_ALLOC_GROW_OR_NULL(arr, old_alloc + 1, alloc);
> + check(arr == NULL);
> + check_uint(alloc, ==, 0);
> + reftable_set_alloc(malloc, realloc, free);
> +
> + reftable_free(arr);
> + }
> +
Thanks for adding this test!
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH v2 3/4] reftable: handle realloc error in parse_names()
2024-12-28 9:48 ` [PATCH v2 3/4] reftable: handle realloc error in parse_names() René Scharfe
@ 2024-12-30 7:25 ` Patrick Steinhardt
0 siblings, 0 replies; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-30 7:25 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List, Junio C Hamano
On Sat, Dec 28, 2024 at 10:48:50AM +0100, René Scharfe wrote:
> Check the final reallocation for adding the terminating NULL and handle
> it just like those in the loop. Simply use REFTABLE_ALLOC_GROW instead
> of keeping the REFTABLE_REALLOC_ARRAY call and adding code to preserve
> the original pointer value around it.
>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> reftable/basics.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/reftable/basics.c b/reftable/basics.c
> index cd6b39dbe9..fe2b83ff83 100644
> --- a/reftable/basics.c
> +++ b/reftable/basics.c
> @@ -241,7 +241,8 @@ char **parse_names(char *buf, int size)
> p = next + 1;
> }
>
> - REFTABLE_REALLOC_ARRAY(names, names_len + 1);
> + if (REFTABLE_ALLOC_GROW(names, names_len + 1, names_cap))
> + goto err;
> names[names_len] = NULL;
Okay. We may be overallocating in some cases now, but it's most likely
that we can completely skip this allocation here because `names_cap` is
big enough already.
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH v2 0/4] reftable: fix realloc error handling
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
` (3 preceding siblings ...)
2024-12-28 9:49 ` [PATCH v2 4/4] t-reftable-merged: handle realloc errors René Scharfe
@ 2024-12-30 7:25 ` Patrick Steinhardt
4 siblings, 0 replies; 25+ messages in thread
From: Patrick Steinhardt @ 2024-12-30 7:25 UTC (permalink / raw)
To: René Scharfe; +Cc: Git List, Junio C Hamano
On Sat, Dec 28, 2024 at 10:43:41AM +0100, René Scharfe wrote:
> Changes since v1:
> - added unit tests
> - explicitly set pointer to NULL on REFTABLE_ALLOC_GROW_OR_NULL failure
> in patch 2; omission found by unit test
>
> reftable: avoid leaks on realloc error
> reftable: fix allocation count on realloc error
> reftable: handle realloc error in parse_names()
> t-reftable-merged: handle realloc errors
I think this version is good enough for now. I'm not particularly happy
about the split we have with the reftable reallocators now, but don't
think that the series really is to blame for that as it simply fixes a
preexisting issue. We can iterate on this in the future.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 25+ messages in thread
end of thread, other threads:[~2024-12-30 7:25 UTC | newest]
Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-12-25 18:33 [PATCH 0/4] reftable: fix realloc error handling René Scharfe
2024-12-25 18:38 ` [PATCH 1/4] reftable: avoid leaks on realloc error René Scharfe
2024-12-27 5:39 ` Junio C Hamano
2024-12-27 10:33 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
2024-12-30 6:29 ` Patrick Steinhardt
2024-12-25 18:38 ` [PATCH 2/4] reftable: fix allocation count " René Scharfe
2024-12-27 10:33 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
2024-12-27 20:16 ` René Scharfe
2024-12-25 18:38 ` [PATCH 3/4] reftable: handle realloc error in parse_names() René Scharfe
2024-12-25 18:38 ` [PATCH 4/4] t-reftable-merged: check realloc errors René Scharfe
2024-12-27 5:46 ` Junio C Hamano
2024-12-27 10:34 ` Patrick Steinhardt
2024-12-27 20:16 ` René Scharfe
2024-12-27 10:34 ` [PATCH 0/4] reftable: fix realloc error handling Patrick Steinhardt
2024-12-27 16:02 ` Junio C Hamano
2024-12-28 9:43 ` [PATCH v2 " René Scharfe
2024-12-28 9:47 ` [PATCH v2 1/4] reftable: avoid leaks on realloc error René Scharfe
2024-12-30 7:25 ` Patrick Steinhardt
2024-12-28 9:48 ` [PATCH v2 2/4] reftable: fix allocation count " René Scharfe
2024-12-28 9:48 ` [PATCH v2 3/4] reftable: handle realloc error in parse_names() René Scharfe
2024-12-30 7:25 ` Patrick Steinhardt
2024-12-28 9:49 ` [PATCH v2 4/4] t-reftable-merged: handle realloc errors René Scharfe
2024-12-30 7:25 ` [PATCH v2 0/4] reftable: fix realloc error handling Patrick Steinhardt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).