URL:
  <https://savannah.gnu.org/bugs/?68570>

                 Summary: [PATCH] [tbl] SEGV when table format description
consists only of horizontal span descriptors
                   Group: GNU roff
               Submitter: gbranden
               Submitted: Mon 27 Jul 2026 01:22:04 AM UTC
                Category: Preprocessor tbl
                Severity: 4 - Important
              Item Group: Crash/Unresponsive
                  Status: In Progress
                 Privacy: Public
             Assigned to: gbranden
             Open/Closed: Open
         Discussion Lock: Unlocked
         Planned Release: None


    _______________________________________________________

Follow-up Comments:


-------------------------------------------------------
Date: Mon 27 Jul 2026 01:22:04 AM UTC By: G. Branden Robinson <gbranden>
This report came to me via private email.

I don't regard it as sensitive.


Hello Branden,

I was fuzzing the groff preprocessors and found a small, reproducible crash
in tbl. It is
a NULL pointer dereference, so a denial of service rather than anything
worse, but I wanted
to send it to you before writing anything up.

When a table's format section marks the first column as horizontally
spanned (s) across all
of its format rows, no table entry ever gets created. tbl correctly prints
the "first column
cannot be horizontally spanned" diagnostic, but then continues into
table::init_output() and
dereferences entry_list, which is still NULL because the table has no
entries.

Reproducer (6 lines), crashes stock /usr/bin/tbl:

.TS
s s
s.
_
x
.TE

$ tbl < crash.tbl >/dev/null
tbl:<standard input>:2: error: first column cannot be horizontally spanned
tbl:<standard input>:3: error: first column cannot be horizontally spanned
Segmentation fault (core dumped)

Reproduced on the groff 1.23.0 shipped in Ubuntu 26.04 (groff-base
1.23.0-10), and the same
code path looks present on current git: table::init_output() still calls
entry_list->set_location() with no NULL check, and I could not find any
entry_list == 0 guard
nearby. There are sibling entry_list->set_location() calls in the file that
look like the same
shape.

This is reachable wherever tbl runs on an untrusted document, for instance
man rendering a
man page that requests table processing, so a crafted page or roff file
crashes tbl. Impact
is limited to the crash.

A guard for the empty-table case would fix it, either skipping the
entry_list->set_location()
calls when entry_list == 0 or returning from init_output() early when the
table has no entries.

I have the reproducer and can share whatever is useful. I will hold off on
any public writeup
until you have looked and a fix is out. Thanks for all your work on groff.

Best regards,
Elias


A few minutes' work produced this patch.


diff --git a/src/preproc/tbl/main.cpp b/src/preproc/tbl/main.cpp
index 82b6c581e..9c878c584 100644
--- a/src/preproc/tbl/main.cpp
+++ b/src/preproc/tbl/main.cpp
@@ -791,6 +791,7 @@ static format *process_format(table_input &in, options
*opt,
   input_entry_format *list = 0 /* nullptr */;
   bool have_expand = false;
   bool is_first_row = true;
+  bool is_first_column = true;
   int c = in.get();
   for (;;) {
     int vrule_count = 0;
@@ -833,8 +834,14 @@ static format *process_format(table_input &in, options
*opt,
        break;
       case 's':
       case 'S':
-       got_format = true;
-       t = FORMAT_SPAN;
+       if (is_first_column)
+         error("cannot horizontally span in first column;"
+               " ignoring column descriptor '%1'",
+               static_cast<char>(c));
+       else {
+         got_format = true;
+         t = FORMAT_SPAN;
+       }
        break;
       case '^':
        got_format = true;
@@ -872,6 +879,8 @@ static format *process_format(table_input &in, options
*opt,
        list = 0 /* nullptr */;
        return 0 /* nullptr */;
       }
+      if (got_format)
+       is_first_column = false;
       if (got_period)
        break;
       c = in.get();
@@ -1180,6 +1189,7 @@ static format *process_format(table_input &in, options
*opt,
     if (c == '\n' || c == ',') {
       vrule_count = 0;
       is_first_row = false;
+      is_first_column = true;
       c = in.get();
       list->is_last_column = true;
     }


All tests pass, but I'll need a new test of course.







    _______________________________________________________

Reply to this item at:

  <https://savannah.gnu.org/bugs/?68570>

_______________________________________________
Message sent via Savannah
https://savannah.gnu.org/

Attachment: signature.asc
Description: PGP signature

Reply via email to