From 1294001ec6cbeea038b15d3a023efd7a6293e81d Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 17:55:16 -0700 Subject: [PATCH 1/8] uloop: fix array reference `ucv_array_new` returns an array with a ref count of 1. `ucv_get(cbs)` adds an extra reference and the array is never free-ed. Signed-off-by: Matthew Cather --- lib/uloop.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/uloop.c b/lib/uloop.c index cd139814..17ff79ad 100644 --- a/lib/uloop.c +++ b/lib/uloop.c @@ -1770,7 +1770,7 @@ uc_uloop_task(uc_vm_t *vm, size_t nargs) cbs = ucv_array_new(NULL); ucv_array_set(cbs, 0, ucv_get(output_cb)); ucv_array_set(cbs, 1, ucv_get(input_cb)); - ucv_resource_value_set(task->cb.obj, 1, ucv_get(cbs)); + ucv_resource_value_set(task->cb.obj, 1, cbs); ok_return(task->cb.obj); } From dd91bd8396a5936ca9d62b97d7299bdf226ac94c Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 20:23:26 -0700 Subject: [PATCH 2/8] tests: add test cases for use-after-free gc bugs 53 - filter/map/callfunc: result/scope freed during a callback gc("collect") 54 - json_from_object: cached read method dangles after obj.read reassignment 55 - zlib def_object/inf_object 56 - socket.poll: result array freed during a user fileno() callback 57 - uvcount=0 resources leaked when reached via a retain=true sweep Signed-off-by: Matthew Cather --- .../99_bugs/53_filter_map_gc_use_after_free | 56 +++++++++++++++++++ .../54_json_read_method_use_after_free | 31 ++++++++++ .../55_zlib_read_method_use_after_free | 39 +++++++++++++ .../99_bugs/56_socket_poll_use_after_free | 31 ++++++++++ .../99_bugs/57_resource_uvcount0_retain_leak | 28 ++++++++++ 5 files changed, 185 insertions(+) create mode 100644 tests/custom/99_bugs/53_filter_map_gc_use_after_free create mode 100644 tests/custom/99_bugs/54_json_read_method_use_after_free create mode 100644 tests/custom/99_bugs/55_zlib_read_method_use_after_free create mode 100644 tests/custom/99_bugs/56_socket_poll_use_after_free create mode 100644 tests/custom/99_bugs/57_resource_uvcount0_retain_leak diff --git a/tests/custom/99_bugs/53_filter_map_gc_use_after_free b/tests/custom/99_bugs/53_filter_map_gc_use_after_free new file mode 100644 index 00000000..8d33481c --- /dev/null +++ b/tests/custom/99_bugs/53_filter_map_gc_use_after_free @@ -0,0 +1,56 @@ +Test that `filter()`, `map()`, and `call()` survive a `gc("collect")` +triggered from inside the user callback. The builtins hold their working +values in C locals while user code runs; under the bug, a GC sweep frees +them and the builtins return corrupted or dangling data. + +-- Testcase -- +{% + let input = []; + for (let i = 0; i < 10; i++) + push(input, i); + + function clobber() { + gc("collect"); + + let trash = []; + for (let i = 0; i < 256; i++) + push(trash, sprintf("clobber_%04d", i)); + } + + let f = filter(input, function(x) { clobber(); return true; }); + printf("filter: type=%s len=%d match=%s\n", + type(f), length(f), (f == null) ? "n/a" : (`${f}` == `${input}` ? "yes" : "no")); + + let m = map(input, function(x) { clobber(); return x; }); + printf("map: type=%s len=%d match=%s\n", + type(m), length(m), (m == null) ? "n/a" : (`${m}` == `${input}` ? "yes" : "no")); + + // uc_callfunc prev_scope UAF: a scope with its own prototype takes the + // branch where fn_scope = ucv_get(scope), with no prototype linkage to + // the prior globals. The prior globals (`prev_scope`) is then held only + // in a C local while user code runs. + global.tag = "original"; + + // capture builtins as closures so the user code can run them after + // vm->globals has been swapped to the prototype-only sandbox scope + let mygc = gc, mypush = push, mysprintf = sprintf; + + call(function() { + mygc("collect"); + let trash = []; + for (let i = 0; i < 256; i++) + mypush(trash, mysprintf("clobber_%04d", i)); + }, null, proto({}, {})); + + // after call() returns, globals must have been restored to a live + // object. accessing global.tag goes through vm->globals; if it is + // dangling the lookup fails or returns garbage. + printf("call: globals.tag=%s\n", global.tag); +%} +-- End -- + +-- Expect stdout -- +filter: type=array len=10 match=yes +map: type=array len=10 match=yes +call: globals.tag=original +-- End -- diff --git a/tests/custom/99_bugs/54_json_read_method_use_after_free b/tests/custom/99_bugs/54_json_read_method_use_after_free new file mode 100644 index 00000000..787a60cf --- /dev/null +++ b/tests/custom/99_bugs/54_json_read_method_use_after_free @@ -0,0 +1,31 @@ +Test that `json()` survives the user's `read()` callback reassigning +`obj.read` between calls. `uc_json_from_object()` caches the read method +as a borrowed pointer for the parse loop; under the bug, reassignment +drops the original closure's last owning reference and the next +iteration dereferences freed memory. + +-- Testcase -- +{% + let chunks = [ '{"hello": ', '"world"}' ]; + let idx = 0; + + let obj = {}; + obj.read = function(n) { + // state-machine pattern: first call swaps in the "real" reader + if (idx == 0) { + obj.read = function(n) { + return idx < length(chunks) ? chunks[idx++] : ""; + }; + } + return idx < length(chunks) ? chunks[idx++] : ""; + }; + + printf("%.J\n", json(obj)); +%} +-- End -- + +-- Expect stdout -- +{ + "hello": "world" +} +-- End -- diff --git a/tests/custom/99_bugs/55_zlib_read_method_use_after_free b/tests/custom/99_bugs/55_zlib_read_method_use_after_free new file mode 100644 index 00000000..a8c85b03 --- /dev/null +++ b/tests/custom/99_bugs/55_zlib_read_method_use_after_free @@ -0,0 +1,39 @@ +Test that `deflate()` and `inflate()` survive the user's `read()` +callback reassigning `obj.read` between calls. The streaming path caches the read method as a borrowed pointer, +and reassignment frees the original closure before the next iteration's +deref. + +-- Testcase -- +{% + import { deflate, inflate } from 'zlib'; + + function make_streaming_reader(chunks) { + let idx = 0; + let obj = {}; + obj.read = function(n) { + // state-machine pattern: first call swaps in the "real" reader + if (idx == 0) { + obj.read = function(n) { + return idx < length(chunks) ? chunks[idx++] : ""; + }; + } + return idx < length(chunks) ? chunks[idx++] : ""; + }; + return obj; + } + + let compressed = deflate(make_streaming_reader([ "hello, ", "world!" ])); + + // slice into 8-byte chunks so inflate() makes multiple read() calls + let chunks = []; + for (let i = 0; i < length(compressed); i += 8) + push(chunks, substr(compressed, i, 8)); + + let r = inflate(make_streaming_reader(chunks)); + printf("%s\n", r); +%} +-- End -- + +-- Expect stdout -- +hello, world! +-- End -- diff --git a/tests/custom/99_bugs/56_socket_poll_use_after_free b/tests/custom/99_bugs/56_socket_poll_use_after_free new file mode 100644 index 00000000..86f0faf6 --- /dev/null +++ b/tests/custom/99_bugs/56_socket_poll_use_after_free @@ -0,0 +1,31 @@ +Test that `socket.poll()` survives a `gc("collect")` triggered from the +user's `fileno()` method. The result array is held in a C local while +user code runs to resolve each polled handle's fd, and a GC sweep frees +it mid-loop. + +-- Testcase -- +{% + import * as socket from 'socket'; + + let s = socket.create(socket.AF_INET, socket.SOCK_STREAM, 0); + let real_fd = s.fileno(); + + let obj = {}; + obj.fileno = function() { + gc("collect"); + let trash = []; + for (let i = 0; i < 256; i++) + push(trash, sprintf("clobber_%04d", i)); + return real_fd; + }; + + let r = socket.poll(0, [obj, socket.POLLIN]); + + printf("type=%s len=%d first_is_obj=%s\n", + type(r), length(r), (length(r) > 0 && r[0][0] === obj) ? "yes" : "no"); +%} +-- End -- + +-- Expect stdout -- +type=array len=1 first_is_obj=yes +-- End -- diff --git a/tests/custom/99_bugs/57_resource_uvcount0_retain_leak b/tests/custom/99_bugs/57_resource_uvcount0_retain_leak new file mode 100644 index 00000000..d42a742f --- /dev/null +++ b/tests/custom/99_bugs/57_resource_uvcount0_retain_leak @@ -0,0 +1,28 @@ +Test that uvcount=0 resources are correctly freed when reached via a +GC sweep. The script builds a self-referential cycle holding a pipe +(whose io.handle resources are uvcount=0) and forces a collection; +under the bug the io.handle structs are leaked on every `gc()` call. + +This test depends on running under a leak detector. + +-- Testcase -- +{% + import * as io from 'io'; + import { stdout } from 'fs'; + + for (let i = 0; i < 3; i++) { + let cycle = []; + cycle[0] = cycle; + cycle[1] = io.pipe(); + cycle = null; + gc("collect"); + } + + print("ok\n"); + stdout.flush(); +%} +-- End -- + +-- Expect stdout -- +ok +-- End -- From d96db3ea5ef2ebfc7ea025f08abe357fec4d94d0 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 21:01:53 -0700 Subject: [PATCH 3/8] json: fix use-after-free of read method across iterations uc_json_from_object cached the read method via ucv_property_get, which returns a borrow. If the user's read callback reassigned obj.read, the original closure's last owning reference vanished when the call frame popped, leaving the cached pointer dangling for the next iteration's ucv_get. Signed-off-by: Matthew Cather --- lib.c | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib.c b/lib.c index f4498cd4..70162260 100644 --- a/lib.c +++ b/lib.c @@ -3463,17 +3463,17 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) { bool trail = false, eof = false; enum json_tokener_error err; - struct json_tokener *tok; - uc_value_t *rfn, *rbuf; + struct json_tokener *tok = NULL; + uc_value_t *rfn, *rbuf = NULL; uc_stringbuf_t *buf; - rfn = ucv_property_get(obj, "read"); + rfn = ucv_get(ucv_property_get(obj, "read")); if (!ucv_is_callable(rfn)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object does not implement read() method"); - return NULL; + goto out; } tok = xjs_new_tokener(); @@ -3483,11 +3483,8 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) uc_vm_stack_push(vm, ucv_get(rfn)); uc_vm_stack_push(vm, ucv_int64_new(1024)); - if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) { - json_tokener_free(tok); - - return NULL; - } + if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) + goto fail; rbuf = uc_vm_stack_pop(vm); @@ -3496,8 +3493,6 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) /* on EOF, stop parsing unless trailing garbage was detected which handled below */ if (eof && !trail) { - ucv_put(rbuf); - /* Didn't parse a complete object yet, possibly a non-delimited atomic value such as `null`, `true` etc. - nudge parser by sending final zero byte. See json-c issue #681 */ @@ -3511,10 +3506,7 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "Trailing garbage after JSON data"); - json_tokener_free(tok); - ucv_put(rbuf); - - return NULL; + goto fail; } if (ucv_type(rbuf) != UC_STRING) { @@ -3536,6 +3528,7 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) } ucv_put(rbuf); + rbuf = NULL; err = json_tokener_get_error(tok); @@ -3543,6 +3536,16 @@ uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) break; } + goto out; + +fail: + json_tokener_free(tok); + tok = NULL; + +out: + ucv_put(rfn); + ucv_put(rbuf); + return tok; } From 8a74d68a34ab286a8c1a08f5de5de6e5fa29c114 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 21:18:53 -0700 Subject: [PATCH 4/8] lib: anchor working values in filter/map/callfunc across user code uc_filter and uc_map hold their result array in a C local; uc_callfunc holds the saved previous globals scope. A gc("collect") triggered from inside the user callback freed those values and left the builtin to dereference a dangling pointer once control returned. Push each onto the VM stack with ucv_get so they're rooted via vm->stack across the call. Signed-off-by: Matthew Cather --- lib.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib.c b/lib.c index 70162260..fb7b6f63 100644 --- a/lib.c +++ b/lib.c @@ -934,6 +934,7 @@ uc_filter(uc_vm_t *vm, size_t nargs) return NULL; arr = ucv_array_new(vm); + uc_vm_stack_push(vm, ucv_get(arr)); for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) { uc_vm_ctx_push(vm); @@ -956,6 +957,8 @@ uc_filter(uc_vm_t *vm, size_t nargs) ucv_put(rv); } + ucv_put(uc_vm_stack_pop(vm)); + return arr; } @@ -1224,6 +1227,7 @@ uc_map(uc_vm_t *vm, size_t nargs) return NULL; arr = ucv_array_new(vm); + uc_vm_stack_push(vm, ucv_get(arr)); for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) { uc_vm_ctx_push(vm); @@ -1243,6 +1247,8 @@ uc_map(uc_vm_t *vm, size_t nargs) ucv_array_push(arr, rv); } + ucv_put(uc_vm_stack_pop(vm)); + return arr; } @@ -5741,7 +5747,7 @@ static uc_value_t * uc_callfunc(uc_vm_t *vm, size_t nargs) { size_t argoff = vm->stack.count - nargs, i; - uc_value_t *fn_scope, *prev_scope, *res; + uc_value_t *fn_scope, *prev_scope = NULL, *res; uc_value_t *fn = uc_fn_arg(0); uc_value_t *this = uc_fn_arg(1); uc_value_t *scope = uc_fn_arg(2); @@ -5767,24 +5773,27 @@ uc_callfunc(uc_vm_t *vm, size_t nargs) fn_scope = NULL; } + if (fn_scope) { + prev_scope = ucv_get(uc_vm_scope_get(vm)); + uc_vm_stack_push(vm, ucv_get(prev_scope)); + uc_vm_scope_set(vm, fn_scope); + } + uc_vm_stack_push(vm, ucv_get(this)); uc_vm_stack_push(vm, ucv_get(fn)); for (i = 3; i < nargs; i++) uc_vm_stack_push(vm, ucv_get(vm->stack.entries[3 + argoff++])); - if (fn_scope) { - prev_scope = ucv_get(uc_vm_scope_get(vm)); - uc_vm_scope_set(vm, fn_scope); - } - if (uc_vm_call(vm, true, i - 3) == EXCEPTION_NONE) res = uc_vm_stack_pop(vm); else res = NULL; - if (fn_scope) + if (fn_scope) { uc_vm_scope_set(vm, prev_scope); + ucv_put(uc_vm_stack_pop(vm)); + } return res; } From 2225a502e805f203c1adbfdc3456e84f726701f9 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 21:29:08 -0700 Subject: [PATCH 5/8] socket: anchor poll() result across user fileno() callbacks A gc("collect") from inside a polled handle's fileno() method freed the C-local result array out from under uc_socket_poll. Push it onto the VM stack so it stays rooted across the call. uc_fn_arg(i) peeks from the top, so it now sees the anchor instead of the args; switch the loop to direct stack-index access via argoff. Signed-off-by: Matthew Cather --- lib/socket.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/socket.c b/lib/socket.c index 980c0964..89c9d1db 100644 --- a/lib/socket.c +++ b/lib/socket.c @@ -2701,11 +2701,14 @@ uc_socket_poll(uc_vm_t *vm, size_t nargs) if (errno != 0 || timeout < (int64_t)INT_MIN || timeout > (int64_t)INT_MAX) err_return(ERANGE, "Invalid timeout value"); + size_t argoff = vm->stack.count - nargs; + rv = ucv_array_new(vm); + uc_vm_stack_push(vm, ucv_get(rv)); for (size_t i = 1; i < nargs; i++) { uc_vector_grow(&pfds); - item = uv_to_pollfd(vm, uc_fn_arg(i), &pfds.entries[pfds.count]); + item = uv_to_pollfd(vm, vm->stack.entries[argoff + i], &pfds.entries[pfds.count]); if (item) ucv_array_set(rv, pfds.count++, item); @@ -2714,6 +2717,7 @@ uc_socket_poll(uc_vm_t *vm, size_t nargs) ret = poll(pfds.entries, pfds.count, timeout); if (ret == -1) { + ucv_put(uc_vm_stack_pop(vm)); ucv_put(rv); uc_vector_clear(&pfds); err_return(errno, "poll()"); @@ -2723,6 +2727,7 @@ uc_socket_poll(uc_vm_t *vm, size_t nargs) ucv_array_set(ucv_array_get(rv, i), 1, ucv_int64_new(pfds.entries[i].revents)); + ucv_put(uc_vm_stack_pop(vm)); uc_vector_clear(&pfds); ok_return(rv); } From 6a061cf4ef34efeb137183edf87df524fefcd381 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 21:32:41 -0700 Subject: [PATCH 6/8] zlib: fix use-after-free of read method across iterations uc_zlib_def_object and uc_zlib_inf_object cached the read method via ucv_property_get, which returns a borrow. If the user's read callback reassigned obj.read, the original closure's last owning reference vanished when the callframe popped, leaving the cached pointer dangling for the next iteration's ucv_get. Take an owning reference and release it at every exit path. Signed-off-by: Matthew Cather --- lib/zlib.c | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/zlib.c b/lib/zlib.c index 83093889..846d0713 100644 --- a/lib/zlib.c +++ b/lib/zlib.c @@ -114,15 +114,15 @@ static bool uc_zlib_def_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zstrm) { int ret; - bool eof = false; - uc_value_t *rfn, *rbuf; + bool eof = false, rv = false; + uc_value_t *rfn, *rbuf = NULL; - rfn = ucv_property_get(obj, "read"); + rfn = ucv_get(ucv_property_get(obj, "read")); if (!ucv_is_callable(rfn)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object does not implement read() method"); - return false; + goto out; } do { @@ -132,7 +132,7 @@ uc_zlib_def_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs uc_vm_stack_push(vm, ucv_int64_new(CHUNK)); if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) - goto fail; + goto out; rbuf = uc_vm_stack_pop(vm); // read output chunk @@ -140,7 +140,7 @@ uc_zlib_def_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs if (rbuf != NULL && ucv_type(rbuf) != UC_STRING) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object read() method returned non-string value"); - goto fail; + goto out; } /* check EOF */ @@ -154,14 +154,16 @@ uc_zlib_def_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs (void)ret; // XXX make annoying compiler that ignores assert() happy ucv_put(rbuf); // release rbuf + rbuf = NULL; } while (!eof); // finish compression if all of source has been read in assert(ret == Z_STREAM_END); // stream will be complete - return true; + rv = true; -fail: +out: ucv_put(rbuf); - return false; + ucv_put(rfn); + return rv; } static bool @@ -322,15 +324,15 @@ static bool uc_zlib_inf_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zstrm) { int ret = Z_STREAM_ERROR; // error out if EOF on first loop - bool eof = false; - uc_value_t *rfn, *rbuf; + bool eof = false, rv = false; + uc_value_t *rfn, *rbuf = NULL; - rfn = ucv_property_get(obj, "read"); + rfn = ucv_get(ucv_property_get(obj, "read")); if (!ucv_is_callable(rfn)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object does not implement read() method"); - return false; + goto out; } do { @@ -340,7 +342,7 @@ uc_zlib_inf_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs uc_vm_stack_push(vm, ucv_int64_new(CHUNK)); if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) - goto fail; + goto out; rbuf = uc_vm_stack_pop(vm); // read output chunk @@ -348,7 +350,7 @@ uc_zlib_inf_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs if (rbuf != NULL && ucv_type(rbuf) != UC_STRING) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object read() method returned non-string value"); - goto fail; + goto out; } /* check EOF */ @@ -364,20 +366,19 @@ uc_zlib_inf_object(uc_vm_t *const vm, uc_value_t * const obj, zstrm_t * const zs case Z_NEED_DICT: case Z_DATA_ERROR: case Z_MEM_ERROR: - goto fail; + goto out; } ucv_put(rbuf); // release rbuf + rbuf = NULL; } while (ret != Z_STREAM_END); // done when inflate() says it's done - if (ret != Z_STREAM_END) // data error - return false; + rv = (ret == Z_STREAM_END); // data error otherwise - return true; - -fail: +out: ucv_put(rbuf); - return false; + ucv_put(rfn); + return rv; } static bool From dbe92bca180b183e092fe4c772898312d45c3693 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 27 Apr 2026 21:33:50 -0700 Subject: [PATCH 7/8] types: free uvcount=0 resources on retain=true sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ucv_free's retain=true path defers free() and expects the GC's second pass to mop up UC_NULL items from vm->values. But resources with uvcount=0 aren't on that list — ucv_resource_new_ex skips them because leaf resources can't form cycles. So a retain=true sweep cascading into one set type=UC_NULL and walked away; the memory leaked forever. Only defer when ref is actually linked into a list. Signed-off-by: Matthew Cather --- types.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.c b/types.c index f66ef40a..ad5e5a9a 100644 --- a/types.c +++ b/types.c @@ -375,7 +375,7 @@ ucv_free(uc_value_t *uv, bool retain) break; } - if (!ref || !retain) { + if (!ref || !retain || !ref->prev || !ref->next) { if (ref && ref->prev && ref->next) ucv_unref(ref); From b0574723d27d7403fbf2fbe10acc7290c946f483 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Mon, 22 Jun 2026 13:11:02 -0700 Subject: [PATCH 8/8] zlib: zero-fill the output reserve window The reserve only wrote one byte at the end of the CHUNK window, so after inflate()/deflate() advanced bpos, buf[bpos] was left uninitialised and the printbuf was no longer NUL terminated. printf("%s") then ran off the end of the data into garbage. json-c 0.16+ masked it by zeroing the gap itself; 0.15 didn't. That one-byte trick avoided zeroing all of CHUNK, but it's pointless now: 0.16+ zero-fills the gap anyway. Just memset the whole window. Signed-off-by: Matthew Cather --- lib/zlib.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/zlib.c b/lib/zlib.c index 846d0713..7d8c150e 100644 --- a/lib/zlib.c +++ b/lib/zlib.c @@ -94,7 +94,7 @@ def_chunks(zstrm_t * const zstrm) /* run deflate() on input until output buffer not full */ do { - printbuf_memset(zstrm->outbuf, printbuf_length(zstrm->outbuf) + CHUNK - 1, 0, 1); + printbuf_memset(zstrm->outbuf, -1, 0, CHUNK); zstrm->outbuf->bpos -= CHUNK; zstrm->strm.avail_out = CHUNK; @@ -299,7 +299,7 @@ inf_chunks(zstrm_t * const zstrm) /* run inflate() on input until output buffer not full */ do { - printbuf_memset(zstrm->outbuf, printbuf_length(zstrm->outbuf) + CHUNK - 1, 0, 1); + printbuf_memset(zstrm->outbuf, -1, 0, CHUNK); zstrm->outbuf->bpos -= CHUNK; zstrm->strm.avail_out = CHUNK;