Skip to content

std.json: json.parse leaks 1 byte per call — empty-string error slot escapes the heap tracker (was: blamed on stringify) #1461

Description

@paul-hammant

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions