Skip to content

dot (upstream C): sametail/samehead on a record node can put the merged port outside every field, and C then routes from uninitialised path-end boxes

Kind
Upstream C behaviour
Severity
medium
Engine
dot
Category
splines
Cases
2
Seeds
1278, 1153
Tracking
GitHub issuewon't fix
Labels
bugfidelityfuzzengine:dotupstream-cwontfix

With sametail (or samehead) on a record node, sameport rounds the merged port to whole points. That can put the port just outside the record's outermost field. C's record_path then finds no field and still returns the side mask without filling in any box, so beginpath/endpath keep whatever endp->boxes/boxn happened to be on the stack. C's edges then start up to 100pt away from the node (seed 1278). In seed 1153 (rankdir=RL) C's Pshortestpath fails, the edge name 86 -> name 87 is lost, and C exits 1. The port uses the node box instead and draws a normal edge from the node boundary.

Reproducer

digraph "fuzz 1278" {
"n3" [shape="record", label="{{{\N}|<p0> long field text|<p1> long field text}}"];
subgraph "cluster_0" {
"n2" [];
"n6" [];
"n4" [];
}
"n0" -> "n3" [penwidth="2.25", weight="1"];
"n2" -> "n4" [label="中文标签\rfoo_bar\rthen", dir="none"];
"n3" -> "n2":"w" [weight="1", sametail="st1"];
"n3" -> "n6" [headport="e", weight="2", sametail="st1"];
{"n1" "n6" "n0"} -> "n0":"nw";
"n4" -> "n6" [headclip="false", lhead="cluster_0"];
}
GVBINDIR=<pango-free plugins> dot -Kdot -Tsvg repro.gv   # C Graphviz 16.1.0
graphviz-pure -Kdot -Tsvg repro.gv                               # port

C output vs port

--- c.svg
+++ rust.svg
-<path fill="none" stroke="black" d="M126,-52C126,-52 134.47,-148.23 88,-194.4 65.44,-216.81 38.89,-187.79 18.75,-212.4 17.16,-214.34 15.69,-216.82 14.65,-219.32"/>
+<path fill="none" stroke="black" d="M126,-119.5C126,-119.5 117.08,-170.99 88,-194.4 63.23,-214.34 38.89,-187.79 18.75,-212.4 17.16,-214.34 15.69,-216.82 14.65,-219.32"/>
-<path fill="none" stroke="black" d="M126,-212.4C126,-212.4 157.86,-51 90.44,-35.23"/>
-<polygon fill="black" stroke="black" points="91.08,-31.78 80.76,-34.17 90.31,-38.74 91.08,-31.78"/>
+<path fill="none" stroke="black" d="M126,-117.5C126,-117.5 122.54,-48.92 90.23,-36.06"/>
+<polygon fill="black" stroke="black" points="91.21,-32.69 80.74,-34.28 89.92,-39.57 91.21,-32.69"/>

Root cause

sameport sets prt.p = {round(x1), round(y1)} (lib/dotgen/sameport.c:149). For the record in the reproducer the boundary point is x = -109.54 relative to the centre, so the rounded port x = -110 is outside field 0 (LL.x = -109.54). record_path (lib/common/shapes.c:3793-3829) loops over the fields, matches none (BETWEEN(ls, p.x, rs) fails) and still return side. beginpath (lib/common/splines.c:542-543, and endpath at :740-741) treats the non-zero return as success and skips the endp->boxes[0] = endp->nb; endp->boxn = 1 fallback. But tend/hend in make_regular_edge are uninitialised locals (pathend_t tend, hend;, lib/dotgen/dotsplines.c:1713), so the box count and boxes are undefined behaviour. C's output is stable from run to run on one build but depends on stack contents.

The port's pboxfn (crates/graphviz-pure-layout/src/dotgen/splinegraph.rs:166-195) deliberately returns None in this case, which falls back to the node box (see the comment there). Confirmed by changing the record to shape=box: both programs then agree on both cases.

Why the port should not match: what C does depends on uninitialised stack memory, and reproducing it would mean modelling a particular C build's stack. Suggest closing as wontfix (upstream bug: record_path should return 0 when no field contains the port, or tend/hend should be zero-initialised).

Cases

2 fuzz case(s): seeds 1278, 1153. Seed 1153 (rankdir=RL, record name 86 with tailport=p0 and sametail) is the exit-status case: C prints triangulation failed, source point not in any triangle, in routesplines, Pshortestpath failed and lost name 86 name 87 edge, then exits 1. The port exits 0 and draws the edge. Regenerate a case with python3 -I scripts/fuzz-gen.py --seed N (checked byte for byte for seed 1278).

Files

tests/fuzz-open/027-dot-sameport-record-uninit-pathend

C and the port

Comparing…
C Graphviz 16.1.0 output, cyan plategraphviz-pure output, magenta plate

C onlyport onlyboth: in register

Reproduce

repro.gv
digraph "fuzz 1278" {
"n3" [shape="record", label="{{{\N}|<p0> long field text|<p1> long field text}}"];
subgraph "cluster_0" {
"n2" [];
"n6" [];
"n4" [];
}
"n0" -> "n3" [penwidth="2.25", weight="1"];
"n2" -> "n4" [label="中文标签\rfoo_bar\rthen", dir="none"];
"n3" -> "n2":"w" [weight="1", sametail="st1"];
"n3" -> "n6" [headport="e", weight="2", sametail="st1"];
{"n1" "n6" "n0"} -> "n0":"nw";
"n4" -> "n6" [headclip="false", lhead="cluster_0"];
}
cmd.txt
-Kdot -Tsvg
cases.txt
r1/diff/s1278.gv seed=1278
r1/diff/s1153.gv seed=1153

Files: c.svg, cases.txt, cmd.txt, repro.gv, rust.svg. Source: tests/fuzz-open/027-dot-sameport-record-uninit-pathend.

All open issues