Talos Vulnerability Report


Diagon GraphPlanar::Write improper array index validation vulnerability

July 5, 2023
CVE Number



An improper array index validation vulnerability exists in the GraphPlanar::Write functionality of Diagon v1.0.139. A specially crafted markdown file can lead to memory corruption. A victim would need to open a malicious file to trigger this vulnerability.


The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Diagon v1.0.139


Diagon - https://github.com/ArthurSonzogni/Diagon


5.3 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L


CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer


Diagon is an interpreter that translates markdown into several formats: latex, planar graph, table, tree and many others.

The Diagon interpreter translates from a representation of a graph to a planar graph. For instance, the input could looks like this:


And the planar graph would result in:

│  D  ││
 │ │┌┴┐│
 │ ││E││
 │ │└┬┘│
 ││ F ││
│   C    │

The input will be parsed and eventually reach the GraphPlanar::Write() function:

void GraphPlanar::Write() {

  if (id_to_name.size() <= 2) {

  int num_vertices = id_to_name.size();

  // Create a graph.
  Graph graph(num_vertices);
  for (auto& it : vertex)
    add_edge(it.from, it.to, graph);

  // Make it connected.
  boost::make_connected(graph);                                                                         [1]

  // Make it biconnected.
  Embedding embedding;
  bool is_planar = ComputePlanarEmbedding(graph, embedding);
  if (!is_planar) {
    output_ = "Graph is not planar.\n";

  boost::make_biconnected_planar(graph, embedding.data());                                              [2]
  Graph biconnected_graph(graph);

  ComputePlanarEmbedding(graph, embedding);
  boost::make_maximal_planar(graph, embedding.data());                                                  [3]

  // Find a canonical ordering.
  ComputePlanarEmbedding(graph, embedding);
  std::vector<boost::graph_traits<Graph>::vertex_descriptor> ordering;
  boost::planar_canonical_ordering(graph, embedding.data(),
                                   std::back_inserter(ordering));                                       [4]

  std::vector<size_t> inverse_ordering(num_vertices);                                                   [5]
  for (int i = 0; i < inverse_ordering.size(); ++i) {
    inverse_ordering[ordering[i]] = i;                                                                  [6]

This function, until the code at [3], will manipulate the graph representation of the input to satisfy the pre-requisites of the planar_canonical_ordering function at [4]. Indeed, to call the planar_canonical_ordering at [4], the graph must be maximal planar; that is, calculated at [3]. Before calling the make_maximal_planar at [3] the graph must be biconnected. To make the graph biconnected, the function make_biconnected_planar, at [2], is called. The last dependency is that the graph must be connected, which is satisfied using make_connected called at [1].

The Graph type is defined as follows:

using Graph = boost::adjacency_list<boost::vecS,
                            boost::property<boost::vertex_index_t, int>,
                            boost::property<boost::edge_index_t, int>>;

The first template parameter is used to specify how to store the edges. In this case, the boost::vecS will store the edges in a std::vector. This configuration will allow parallel edges. For instance, in the example bellow, we have the edge (C,D) but also (D,E).

Using the boost::vecS, allegedly, will break some assumption of the called function. For instance, it could create a graph that is not biconnected at [2]. This can lead to an out-of-bounds read and write. In the POC shown below, after calling the planar_canonical_ordering function at [4] the ordering will have a number of elements lower than the num_vertices variable that represent the number of vertices in the graph. Because inverse_ordering, at [5] , is created based on the actual number of vertices, the inverse_ordering buffer and the ordering one will mismatch in sizes. This would cause, at [6], an out-of-bounds read and write, possibly resulting in a memory corruption.

Exploit Proof of Concept

Following the content of the POC used:

$ cat POC.md

If we run Diagon compile with ASAN with the POC:

$ diagon GraphPlanar < POC.md
==3943598==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x0000006952ef bp 0x7ffddd10d730 sp 0x7ffddd10c7e0 T0)
==3943598==The signal is caused by a READ memory access.
==3943598==Hint: this fault was caused by a dereference of a high value address (see register values below).  Dissassemble the provided pc to learn which register was used.
    #0 0x6952ef in GraphPlanar::Write() /home/vagrant/Diagon/src/translator/graph_planar/GraphPlanar.cpp:342:35
    #1 0x693085 in GraphPlanar::Translate(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /home/vagrant/Diagon/src/translator/graph_planar/GraphPlanar.cpp:194:3
    #2 0x4fbd0e in (anonymous namespace)::Translate(Translator*, int, char const**) /home/vagrant/Diagon/src/main.cpp:277:36
    #3 0x4f97e7 in main /home/vagrant/Diagon/src/main.cpp:326:12
    #4 0x7fe4a9895d09 in __libc_start_main csu/../csu/libc-start.c:308:16
    #5 0x44ca69 in _start (/home/vagrant/Diagon/results/ordering_graphplanar_write_error/diagon_unfixed+0x44ca69)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/vagrant/Diagon/src/translator/graph_planar/GraphPlanar.cpp:342:35 in GraphPlanar::Write()

We can see that a segmentation fault occured. The GraphPlanar.cpp:342 line corresponds to the code at [6]


The maintainer has provided a patch at: https://github.com/ArthurSonzogni/Diagon/releases/tag/v1.1.158


2023-05-03 - Vendor Disclosure
2023-05-08 - Vendor Patch Release
2023-07-05 - Public Release


Discovered by Francesco Benvenuto of Cisco Talos.