From 4a1f36cb80b489de6536171bcd428ac5f63df103 Mon Sep 17 00:00:00 2001 From: Daniel Markstedt Date: Sat, 7 Mar 2026 09:13:34 +0100 Subject: [PATCH 1/2] fix bUuDecodeEx: prevent OOM and memory leak on malformed input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bsread was called with INT_MAX as the byte limit, allowing up to 2 GB to accumulate in the output bstring when the UU decode stream failed to signal EOF cleanly on input lacking line terminators. Cap the read at src->slen, which is always a safe upper bound since decoded output is smaller than its encoded input. The same function also leaked the bsUuCtx and its two internal bstrings whenever bsUuDecodePart was not invoked before the stream was closed — in particular when src->slen is zero, causing bsread to return immediately without calling the read function. The self-freeing pattern in bsUuDecodePart was inherently fragile: it freed ctx inside the callback, leaving bsclose to return a dangling pointer that bUuDecodeEx silently discarded. Fix by transferring ownership explicitly: bsUuDecodePart now only frees the stream buffers (ctx->io.dst, ctx->io.src) at EOF and nulls them; bUuDecodeEx always frees the ctx container via bsclose's return value. Since bdestroy(NULL) is a no-op, the exit path handles both the fully-consumed and early-close cases safely. Both bugs were detected by libFuzzer fuzz testing. --- bstring/bstraux.c | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/bstring/bstraux.c b/bstring/bstraux.c index 92d57fe..76bf22b 100644 --- a/bstring/bstraux.c +++ b/bstring/bstraux.c @@ -648,10 +648,16 @@ bUuDecLine(void *parm, int ofs, int len) return ret; } +struct bsUuCtx { + struct bUuInOut io; + struct bStream * sInp; +}; + bstring bUuDecodeEx(const bstring src, int *badlines) { struct bStream *s, *d; + struct bsUuCtx *ctx; struct tagbstring t; bstring b; @@ -668,11 +674,22 @@ bUuDecodeEx(const bstring src, int *badlines) if (NULL == b) { goto error; } - if (0 > bsread(b, d, INT_MAX)) { + if (src->slen > 0 && 0 > bsread(b, d, src->slen)) { goto error; } exit: - bsclose(d); + /* + * bsUuDecodePart frees ctx->io.dst and ctx->io.src when it signals + * EOF (nulling both pointers), but leaves ctx itself for us to free. + * If the stream was never read (e.g. empty input), all three are still + * live. Either way, bsclose returns the ctx pointer and we own it. + */ + ctx = (struct bsUuCtx *)bsclose(d); + if (ctx) { + bdestroy(ctx->io.dst); + bdestroy(ctx->io.src); + free(ctx); + } bsclose(s); return b; error: @@ -681,11 +698,6 @@ bUuDecodeEx(const bstring src, int *badlines) goto exit; } -struct bsUuCtx { - struct bUuInOut io; - struct bStream * sInp; -}; - static size_t bsUuDecodePart(void *buff, size_t elsize, size_t nelem, void *parm) { @@ -746,10 +758,11 @@ bsUuDecodePart(void *buff, size_t elsize, size_t nelem, void *parm) return tsz; } } - /* Deallocate once EOF becomes triggered */ + /* Release stream buffers on EOF; ctx itself is owned by bUuDecodeEx. */ bdestroy(ctx->io.dst); + ctx->io.dst = NULL; bdestroy(ctx->io.src); - free(ctx); + ctx->io.src = NULL; return 0; } From 359515b86ac29337e7c82af439086477146c3950 Mon Sep 17 00:00:00 2001 From: Daniel Markstedt Date: Sat, 7 Mar 2026 14:49:21 +0100 Subject: [PATCH 2/2] unit test for: bUuDecodeEx: prevent OOM and memory leak on malformed input --- tests/testaux.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/testaux.c b/tests/testaux.c index ff80d96..9fdde74 100644 --- a/tests/testaux.c +++ b/tests/testaux.c @@ -510,6 +510,22 @@ START_TEST(core_014) } END_TEST +START_TEST(core_016) +{ + /* Empty input: bUuDecodeEx must return an empty bstring (not NULL) + * and must not leak the internal decode context. Before the fix, + * bsread was called with INT_MAX on empty input, immediately returned + * BSTR_ERR, and the function took the error path — returning NULL and + * leaking ctx, ctx->io.dst and ctx->io.src. */ + struct tagbstring empty = bsStatic(""); + int err = 0; + bstring c = bUuDecodeEx(&empty, &err); + ck_assert_ptr_nonnull(c); + ck_assert_int_eq(c->slen, 0); + bdestroy(c); +} +END_TEST + int main(void) { @@ -532,6 +548,7 @@ main(void) tcase_add_test(core, core_012); tcase_add_test(core, core_013); tcase_add_test(core, core_014); + tcase_add_test(core, core_016); suite_add_tcase(suite, core); /* Run tests */ SRunner *runner = srunner_create(suite);