* src/pr.c: Continue processing if the line is truncated instead of
returning early. Return after handling headers, line numbers, etc., but
before printing the truncated clump.
* tests/pr/inf-loop.sh: New file.
* tests/local.mk (all_tests): Add the new test case.
* NEWS: Mention the bug fix.

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-TMZPQ2AH.
---
 NEWS                 |  5 +++++
 src/pr.c             | 15 ++++++++++-----
 tests/local.mk       |  1 +
 tests/pr/inf-loop.sh | 37 +++++++++++++++++++++++++++++++++++++
 4 files changed, 53 insertions(+), 5 deletions(-)
 create mode 100755 tests/pr/inf-loop.sh

diff --git a/NEWS b/NEWS
index 728529c31..5e9360c23 100644
--- a/NEWS
+++ b/NEWS
@@ -142,6 +142,11 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   than INT_MAX bytes of column data for a single page.
   [This bug was present in "the beginning".]
 
+  'pr' no longer loops infinitely when the first character of each line in a 
row
+  is wider than a column.  E.g., previously, "yes $'\t' | head -n 56 | pr -W 
7'"
+  would loop infinitely instead of printing an empty page.
+  [This bug was present in "the beginning".]
+
   'realpath' without -s, or 'readlink' with -e,-f,-m no longer
   loop infinitely when resolving a symbolic link which
   resolves to a path beginning with the symbolic link itself.
diff --git a/src/pr.c b/src/pr.c
index e0140e567..561e51696 100644
--- a/src/pr.c
+++ b/src/pr.c
@@ -2477,11 +2477,13 @@ read_line (COLUMN *p)
       chars = char_to_clump (c);
     }
 
-  if (truncate_lines && input_position > chars_per_column)
-    {
-      input_position = last_input_position;
-      return false;
-    }
+  /* If the first clump does not fit in the column, account for it but do
+     not print it.  We must still handle headers, line numbers, etc., so
+     continue instead of returning.  */
+  bool const first_clump_fits = ! (truncate_lines
+                                   && chars_per_column < input_position);
+  if (! first_clump_fits)
+    input_position = last_input_position;
 
   if (p->char_func != store_char)
     {
@@ -2525,6 +2527,9 @@ read_line (COLUMN *p)
   if (c == '\n')
     return true;
 
+  if (! first_clump_fits)
+    return false;
+
   print_clump (p, chars, clump_buff);
 
   while (true)
diff --git a/tests/local.mk b/tests/local.mk
index 33d830cd6..b03ea397e 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -307,6 +307,7 @@ all_tests =                                 \
   tests/mktemp/write-error.sh                  \
   tests/misc/arch.sh                           \
   tests/pr/bounded-memory.sh                   \
+  tests/pr/inf-loop.sh                         \
   tests/pr/options.sh                          \
   tests/pr/pr-tests.pl                         \
   tests/pwd/argument.sh                                \
diff --git a/tests/pr/inf-loop.sh b/tests/pr/inf-loop.sh
new file mode 100755
index 000000000..77fb3ae2c
--- /dev/null
+++ b/tests/pr/inf-loop.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Test a case that would make 'pr' infinite loop in coreutils-9.12 and earlier.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ pr
+
+# Test that we don't have an infinite loop when the input line length is longer
+# than the page width.
+yes "$(printf '\t')" | head -n 100 >inp &&
+tr -d '\t' <inp >exp || framework_failure_
+pr -t -W 7 inp >out 2>err || fail=1
+compare exp out || fail=1
+compare /dev/null err || fail=1
+
+# Make sure we don't lose line numbers.
+truncate -s 0 exp || framework_failure_
+for i in $(seq 100); do printf '%5d\t\n' "$i" >>exp || framework_failure_; done
+pr -n -t -W 7 inp >out 2>err || fail=1
+compare exp out || fail=1
+compare /dev/null err || fail=1
+
+Exit $fail
-- 
2.56.0


Reply via email to