Symptom
Every json.stringify() call leaks exactly 1 byte. Valgrind, on an 8-line program that touches nothing but the JSON API:
import std.string
import std.json
main() {
doc, perr = json.parse("{\"x\":3.14}")
string.free(perr)
out, serr = json.stringify(doc)
string.free(out)
string.free(serr)
json.free(doc)
}
1 bytes in 1 blocks are definitely lost in loss record 1 of 1
at malloc (vg_replace_malloc.c:381)
by aether_caps_malloc
by main
definitely lost: 1 bytes in 1 blocks
Every documented free is called. The leak is inside stringify.
Cause
std/json/module.ae:156:
stringify(value: ptr) -> string! {
raw = json_stringify_raw(value)
if raw == null {
return string_concat("", ""), string_concat("stringify failed", "")
}
return string_concat(raw, ""), string_concat("", "")
}
json_stringify_raw returns an owned allocation; string_concat(raw, "") copies it into a fresh heap string and raw is then dropped without being freed. (The string_concat(x, "") idiom is deliberate — it mints a properly-owned AetherString from a plain char* so the heap tracker will free it — but the source still needs releasing.)
The 1-byte size is a red herring from the allocator's accounting; the point is one unfreed block per call, so a serialisation loop leaks linearly.
Not a regression
Confirmed on pristine main (fab571bd) built in a clean git worktree — identical result. Found incidentally while chasing the macOS leaks gate in #1459; explicitly not caused by that PR, which is why it is filed separately rather than fixed there.
Why it went unnoticed
- The
.ae regression suite is not leak-gated on Linux; only the macOS ARM64 leaks job and the valgrind legs would see it, and no existing leak-gated test round-trips through stringify.
- 1 byte per call is below the noise floor of a casual look at a valgrind summary.
Suggested fix
Free raw after the copy, e.g. bind it and release it on both return paths (checking whether the correct call is string_free or string_release for a json_stringify_raw return — the two are not interchangeable here).
Worth a leak-gated regression test that does a parse → stringify → free round-trip, since nothing currently covers it.
Impact
Matters most for anything serialising in a loop — HTTP handlers returning JSON, avn-style commit walks, the tinyweb/schema showcase. A long-running server leaks one block per response.
Symptom
Every
json.stringify()call leaks exactly 1 byte. Valgrind, on an 8-line program that touches nothing but the JSON API:Every documented free is called. The leak is inside
stringify.Cause
std/json/module.ae:156:json_stringify_rawreturns an owned allocation;string_concat(raw, "")copies it into a fresh heap string andrawis then dropped without being freed. (Thestring_concat(x, "")idiom is deliberate — it mints a properly-ownedAetherStringfrom a plainchar*so the heap tracker will free it — but the source still needs releasing.)The 1-byte size is a red herring from the allocator's accounting; the point is one unfreed block per call, so a serialisation loop leaks linearly.
Not a regression
Confirmed on pristine
main(fab571bd) built in a cleangit worktree— identical result. Found incidentally while chasing the macOS leaks gate in #1459; explicitly not caused by that PR, which is why it is filed separately rather than fixed there.Why it went unnoticed
.aeregression suite is not leak-gated on Linux; only the macOS ARM64 leaks job and the valgrind legs would see it, and no existing leak-gated test round-trips throughstringify.Suggested fix
Free
rawafter the copy, e.g. bind it and release it on both return paths (checking whether the correct call isstring_freeorstring_releasefor ajson_stringify_rawreturn — the two are not interchangeable here).Worth a leak-gated regression test that does a parse → stringify → free round-trip, since nothing currently covers it.
Impact
Matters most for anything serialising in a loop — HTTP handlers returning JSON, avn-style commit walks, the tinyweb/schema showcase. A long-running server leaks one block per response.