Daniel Koh Ji Yang via bug-diffutils via "All diffutils discussion."
<[email protected]> writes:

> A signed-overflow bug in diffutils 3.12, found by automated testing under 
> UBSan. I don't have a local diffutils build; I confirmed the lines by reading 
> the pinned 3.12 source (git.savannah.gnu.org, tag v3.12) and current master - 
> both still have it - against the replay's output.
>
> diff's old-style numeric option (a bare "-<digits>", e.g. "diff -3") is 
> parsed digit-by-digit in src/diff.c with checked arithmetic that correctly 
> clamps an overflowing digit string to LIN_MAX (the max the `lin` type holds). 
> That clamped value flows into context and then horizon_lines. The problem is 
> that LIN_MAX itself reaches src/io.c:1206 in find_identical_ends(), which 
> computes:
>
>   lin i = horizon_lines + !((buffer0 == p0 || p0[-1] == '\n') && (buffer1 == 
> p1 || p1[-1] == '\n'));
>
> When horizon_lines == LIN_MAX and the boolean term is 1 (the common case), 
> that's LIN_MAX + 1, which overflows.
>
>   diff -u -999999999999999999999999999999 A B
>   io.c:1206:29: runtime error: signed integer overflow: 9223372036854775807 + 
> 1 cannot be represented in type 'long'
>       find_identical_ends <- read_files <- diff_2_files <- ... <- main
>
> On a normal build this wraps to a large negative value that then bounds a 
> "while (i-- && p0 != end0)" loop; I didn't establish whether that wrapped 
> bound can walk end0 past the buffer (would need a non-trapping build to 
> observe), so I'm reporting the overflow itself, not a memory-safety claim.
>
> Fix: keep the clamp below the type's true maximum (clamp old-style digit 
> strings and --horizon-lines to LIN_MAX - 1, or whatever margin the largest 
> downstream "+1" needs), or compute the boolean term first at io.c:1206 and 
> use saturating addition, rather than relying on horizon_lines never actually 
> reaching LIN_MAX.
>
> Found by replaying automatically generated tests under ASan/UBSan.

Thanks for the report. I attached a patch which should fix it. Limiting
the value to LIN_MAX - 1 seems simpler than trying to handle the
overflow when we see it. I also doubt anyone will complain about the
possible output context being lowered by 1 in this case.

It looks like it was introduced in 3.11, from the following commit:

    $ git log 6341b346cd0c74c344f4e1c5c7f3556267547fe2 -n 1
    commit 6341b346cd0c74c344f4e1c5c7f3556267547fe2
    Author:     Paul Eggert <[email protected]>
    AuthorDate: Sat Jun 3 15:24:54 2023 -0700
    Commit:     Paul Eggert <[email protected]>
    CommitDate: Sat Jun 3 15:50:33 2023 -0700
    
        diff: don’t limit contexts arbitrarily
        
        * src/context.c (find_hunk):
        Check for overflow when computing non-ignorable threshold.
        * src/diff.c (main): Limit ‘context’ to LIN_MAX since it is a lin.
        * src/system.h (CONTEXT_MAX): Remove.
    $ git tag --contains 6341b346cd0c74c344f4e1c5c7f3556267547fe2 | sort 
--version-sort 
    v3.11
    v3.12

Similar to you, I didn't see any memory saftey issues from my testing.
Things seemed to behave fine aside from UBSAN rightfully complaining.

Collin

>From 02ca82eb40713a8bc70071f7a2b3b8bd1adfc22c Mon Sep 17 00:00:00 2001
Message-ID: <02ca82eb40713a8bc70071f7a2b3b8bd1adfc22c.1790552134.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Sun, 27 Sep 2026 16:21:44 -0700
Subject: [PATCH] diff: fix an integer overflow

* NEWS: Mention the bug fix.
* src/diff.c (main): Limit OCONTEXT to LIN_MAX - 1.
* tests/basic: Add a test case that triggers UBSAN without the fix.
* THANKS: Add Daniel Koh Ji Yang for reporting the bug.
Fixes https://bugs.gnu.org/81937
---
 NEWS        |  3 +++
 THANKS      |  1 +
 src/diff.c  |  7 +++++--
 tests/basic | 14 ++++++++++++++
 4 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/NEWS b/NEWS
index f92bed9..a8288bc 100644
--- a/NEWS
+++ b/NEWS
@@ -9,6 +9,9 @@ GNU diffutils NEWS                                    -*- outline -*-
   or unexpectedly truncated data.
   [bugs present since "the beginning"]
 
+  diff -u no longer overflows a signed integer when PTRDIFF_MAX lines of
+  output context are requested.
+  [bug#81937 introduced in 3.11]
 
 * Noteworthy changes in release 3.12 (2025-04-08) [stable]
 
diff --git a/THANKS b/THANKS
index a372954..61b95c8 100644
--- a/THANKS
+++ b/THANKS
@@ -21,4 +21,5 @@ Randy Smith  <[email protected]>
 Richard Stallman  <[email protected]>
 Leonard H. Tower Jr.  <[email protected]>
 Larry Wall  <[email protected]>
+Daniel Koh Ji Yang  <[email protected]>
 Eli Zaretskii  <[email protected]>
diff --git a/src/diff.c b/src/diff.c
index 002a22d..6ffeeea 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -360,9 +360,12 @@ main (int argc, char **argv)
       case '9':
 	if (! c_isdigit (prev))
 	  ocontext = 0;
+        /* Limit OCONTEXT to LIN_MAX - 1, so we can add 1 to
+           HORIZON_LINES in find_identical_ends.  */
 	if (ckd_mul (&ocontext, ocontext, 10)
-	    || ckd_add (&ocontext, ocontext, c - '0'))
-	  ocontext = LIN_MAX;
+	    || ckd_add (&ocontext, ocontext, c - '0')
+            || ocontext == LIN_MAX)
+	  ocontext = LIN_MAX - 1;
 	break;
 
       case 'a':
diff --git a/tests/basic b/tests/basic
index 161262d..519d165 100755
--- a/tests/basic
+++ b/tests/basic
@@ -39,4 +39,18 @@ for opt in '' -u -c; do
   compare exp-$(echo $opt|tr ' ' _) out || fail=1
 done
 
+# This would overflow a signed integer in diffutils-3.12 and earlier.
+returns_ 1 diff -u -9999999999999999999999999999 a b >out-t 2>err || fail=1
+# Remove date and time.
+sed -e 's/^\([-+*][-+*][-+*] [^	]*\)	.*/\1/' out-t >out || framework_failure_
+cat <<\EOF >exp || framework_failure
+--- a
++++ b
+@@ -1 +1 @@
+-a
++b
+EOF
+compare exp out || fail=1
+compare /dev/null err || fail=1
+
 Exit $fail
-- 
2.55.0

Reply via email to