Skip to content

neato/fdp/sfdp: unseeded start=random (and fdp start=regular) is seeded from the clock in C; the port uses a fixed seed

Kind
Upstream C behaviour
Severity
low
Engine
neato
Category
layout
Cases
11
Seeds
584, 787, 821, 938, 1213, 1259, 1308, 10601, 10718, 11850, 12031
Tracking
GitHub issuewon't fix
Labels
fidelityfuzzengine:neatoengine:fdpengine:sfdpupstream-cwontfix

With start=random and no number after it (neato, fdp, sfdp), or fdp with start=regular or start=self, C seeds its random number generator from the clock, time(NULL). The same input therefore gives a different layout in C every second. The port uses a fixed seed instead (1, the value C uses when no start is given), so its output is deterministic. It can match C only by chance, so these cases are not port bugs. Rounds 1 and 2 of the fuzzer generated such inputs before the generator was changed to always seed start=random (commit 6b4ca32) and to avoid fdp's regular/self (commit 5867090).

Reproducer

digraph G {
layout=neato;
start=random;
a -> b;
}
GVBINDIR=<pango-free plugins> dot -Kneato -Tsvg repro.gv   # C Graphviz 16.1.0: changes from one second to the next
graphviz-pure -Kneato -Tsvg repro.gv                              # port: always the same

repro-fdp.gv shows the fdp start=regular variant (-Kfdp).

C output vs port

Two C runs 1.2 s apart (c.1.svg, c.2.svg):

--- c.1.svg	2026-10-19 09:47:57
+++ c.2.svg	2026-10-19 09:47:58
@@ -4,28 +4,28 @@
 <!-- Generated by graphviz version 16.1.0 (20260904.0139)
  -->
 <!-- Title: G Pages: 1 -->
-<svg width="112pt" height="95pt"
- viewBox="0.00 0.00 112.00 95.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 91.38)">
+<svg width="122pt" height="84pt"
+ viewBox="0.00 0.00 122.00 84.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 79.57)">
 <title>G</title>
-<polygon fill="white" stroke="none" points="-4,4 -4,-91.38 108.44,-91.38 108.44,4 -4,4"/>
+<polygon fill="white" stroke="none" points="-4,4 -4,-79.57 118.15,-79.57 118.15,4 -4,4"/>
 <!-- a -->
 <g id="node1" class="node">
 <title>a</title>
-<ellipse fill="none" stroke="black" cx="27" cy="-69.38" rx="27" ry="18"/>
-<text xml:space="preserve" text-anchor="middle" x="27" y="-65.18" font-family="Times,serif" font-size="14.00">a</text>
+<ellipse fill="none" stroke="black" cx="27" cy="-18" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="27" y="-13.8" font-family="Times,serif" font-size="14.00">a</text>
 </g>
 <!-- b -->
 <g id="node2" class="node">
 <title>b</title>
-<ellipse fill="none" stroke="black" cx="77.44" cy="-18" rx="27" ry="18"/>
-<text xml:space="preserve" text-anchor="middle" x="77.44" y="-13.8" font-family="Times,serif" font-size="14.00">b</text>
+<ellipse fill="none" stroke="black" cx="87.15" cy="-57.57" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="87.15" y="-53.37" font-family="Times,serif" font-size="14.00">b</text>

With an explicit seed (start=random1), C and the port are byte-identical on the reproducer and on all six start=random cases below.

Root cause

C, lib/neatogen/neatoinit.c:942-948 (setSeed, used by neato, fdp and sfdp): with INIT_RANDOM and no digits after random, seed = (unsigned) time(NULL). C then writes the seed back with agset(G, "start", ...), which does not reach SVG output. C, lib/fdpgen/tlayout.c:481-487 (init_params): when the mode is not INIT_RANDOM (regular, self), local_seed = (long)time(NULL) before srand48.

Port, crates/graphviz-pure-layout/src/neatogen/neatoinit.rs:130-139 (set_seed): uses seed 1 when no number follows. crates/graphviz-pure-layout/src/fdpgen/tlayout.rs:465-471: uses DFLT_SEED instead of the clock. Both are deliberate, so the port stays reproducible.

This is intended behaviour on both sides, not a bug to fix. Close as wontfix. A test against C has to give start a seed (start=randomN or start=N), and fdp tests should not use start=regular|self.

Cases

11 fuzz cases from rounds 1 and 2: start=random with neato (1259, 787), fdp (1213, 1308) and sfdp (821, 584), and fdp start=regular (10718, 10601, 938, 11850, 12031). Each start=random case was confirmed by replacing the value with random1, after which the port matched C. Each start=regular case was confirmed by running C three times 1.1 s apart: C gave three different outputs every time. The current generator no longer produces these inputs, so python3 -I scripts/fuzz-gen.py --seed N gives a different case for these seeds. Use the archived case files listed in cases.txt.

Files

tests/fuzz-open/029-neato-fdp-start-time-seeded

C and the port

No pair of C and port outputs in this reproducer.

Reproduce

repro.gv
digraph G {
layout=neato;
start=random;
a -> b;
}
cmd.txt
-Kneato -Tsvg (fdp: -Kfdp -Tsvg on repro-fdp.gv)
cases.txt
r1/diff/s1213.gv seed=1213
r1/diff/s1308.gv seed=1308
r1/diff/s1259.gv seed=1259
r1/diff/s787.gv seed=787
r1/diff/s821.gv seed=821
r1/diff/s584.gv seed=584
r2/diff/s10718.gv seed=10718
r2/diff/s10601.gv seed=10601
r1/diff/s938.gv seed=938
r2/diff/s11850.gv seed=11850
r2/diff/s12031.gv seed=12031

Files: c.1.svg, c.2.svg, cases.txt, cmd.txt, repro-fdp.gv, repro.gv, rust.svg. Source: tests/fuzz-open/029-neato-fdp-start-time-seeded.

All open issues