-Tplain: C prints freed memory for Latin-1/entity-converted labels (use-after-free in canon)
With -Tplain and -Tplain-ext, C prints freed memory in place of a node label when the label text changed during conversion (latin1ToUTF8 for charset=latin1, or htmlEntityUTF8 for entities such as ∞) and does not need quoting. The label field then contains whatever the freed allocation holds: on macOS a control byte that differs between runs (^E, ^F, ...). The port prints the converted label (été).
Reproducer (the file is Latin-1: the label bytes are E9 74 E9)
graph G {
charset=latin1
a [label="\xe9t\xe9"]
}
GVBINDIR=<pango-free plugins> dot -Tplain repro.gv # two different outputs over 6 runs
graphviz-pure -Tplain repro.gv
C output vs port
--- c.plain
+++ rust.plain
@@ -1,3 +1,3 @@
graph 1 0.75 0.5
-node a 0.375 0.25 0.75 0.5 \x05 solid ellipse black lightgrey
+node a 0.375 0.25 0.75 0.5 été solid ellipse black lightgrey
stop
Root cause
canon (lib/common/output.c:103-108) interns the label with agstrdup and passes it to agstrcanon, then frees it with agstrfree. When the string needs no quotes, _agstrcanon returns its argument itself (lib/cgraph/write.c:202, return arg;) instead of the scratch buffer. If nothing else holds a reference to the interned string (the converted text is new), agstrfree releases it and writenodeandport/write_plain (lib/common/output.c:153-158, and line 202 for edge labels) print a dangling pointer. This is a use-after-free in C. The port (crates/graphviz-pure-render/src/emit/output.rs:219-221, canon_str) prints the converted text, which is what C intends.
Why the port should not match: the C output is undefined behaviour and changes between runs. gv-diff render skips these references (crates/graphviz-pure-diff/src/render.rs:21-46, UNSTABLE). This should be reported upstream: canon should copy the result before agstrfree.
Cases
10 render-corpus references per format (plain and plain-ext): charset_Latin1, charset_b34, charset_b56, charset_b60, charset_latin1_html, charset_latin1_mixed, gallery__index_7_fe2a40, gv16_graphs_Symbol_af1a2d, gv16_graphs_b34_ca01c7, gv16_graphs_b56_36f937.
Files
tests/fuzz-open/033-latin1-plain-freed-label
C and the port
Reproduce
graph G {charset=latin1a [label="�t�"]}GVBINDIR=$HOME/src/gv-ref/lutplugins dot -Tplain repro.gvgraphviz-pure -Tplain repro.gvtests/wild/charset_Latin1.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/charset_b34.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/charset_b56.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/charset_b60.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/charset_latin1_html.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/charset_latin1_mixed.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/gallery__index_7_fe2a40.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/gv16_graphs_Symbol_af1a2d.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/gv16_graphs_b34_ca01c7.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)tests/wild/gv16_graphs_b56_36f937.gv (-Tplain, -Tplain-ext; listed in crates/graphviz-pure-diff/src/render.rs UNSTABLE)Files: c.plain, cases.txt, cmd.txt, repro.gv, rust.plain. Source: tests/fuzz-open/033-latin1-plain-freed-label.