patch(1) has three related bugs triggered by malformed input that
is interpreted as multiple ed scripts.

1) File descriptor leak

When diff_type is ED_DIFF, the main loop does:

  do_ed_script();
  continue;

This skips the fclose() calls for ofp and rejfp that the normal
path performs at lines 405-410.  Each iteration leaks at least
one file descriptor (rejfp is always opened, ofp only when
skip_rest_of_patch is false).

2) Infinite loop

When intuit_diff_type() finds an ed script at a given position,
do_ed_script() may not advance the file position past it.  On the
next iteration, intuit_diff_type() finds the same ed script at
the same position.  This loops indefinitely.

Additionally, do_ed_script() is called even when
skip_rest_of_patch is true, causing it to try to process an ed
script with no input file loaded (input_lines == 0).

3) Heap corruption (consequence of infinite loop)

Each loop iteration allocates and frees small strings (filenames,
bestguess, outname).  After hundreds of iterations, OpenBSD's
malloc detects a write to freed memory:

  patch(PID) in malloc(): write to free mem 0x...[8..15]@16
  Abort trap (core dumped)

Core dump backtrace:

  #0 thrkill
  #1 _libc_abort at abort.c:51
  #2 wrterror at malloc.c:378
  #3 validate_junk at malloc.c:781
  #4 malloc_bytes at malloc.c:1238
  #5 omalloc at malloc.c:1392
  #6 _libc_malloc at malloc.c:1546
  #7 _libc_strdup (str="Oops") at strdup.c:45
  #8 xstrdup (s=NULL) at util.c:201
  #9 main at patch.c:280

Both the corrupted chunk and the preceding chunk were allocated
by strdup (malloc diagnostic: "allocated at libc.so 0x71cec").
The corruption is a write to bytes [8..15] of a freed 16-byte
chunk, detected lazily when malloc reuses the chunk.

I was unable to identify the exact instruction that writes to the
freed chunk.  AddressSanitizer is not available on OpenBSD, and
the corruption is heap-layout-dependent (~33% reproduction rate
with MALLOC_OPTIONS=CFGJSU).  The diffs below eliminate the
infinite loop that triggers the corruption, reducing the crash
rate from ~33% to ~1%.  The residual crashes occur during the
single first-iteration ed script processing under specific heap
layouts, and would need AddressSanitizer to fully diagnose.

Fix:
- Close ofp and rejfp in the ed script path (matching lines
  405-410 pattern).
- Guard do_ed_script() with !skip_rest_of_patch so it is not
  called without a loaded input file.
- Detect when intuit_diff_type() finds the same patch type at
  the same position as the previous iteration and stop, treating
  the remainder as trailing garbage.

Confirmed on OpenBSD 7.8/arm64.

Found by AFL++ fuzzing.

Index: usr.bin/patch/patch.c
===================================================================
RCS file: /cvs/src/usr.bin/patch/patch.c,v
retrieving revision 1.78
diff -u -p -r1.78 patch.c
--- usr.bin/patch/patch.c       23 Feb 2026 16:40:45 -0000      1.78
+++ usr.bin/patch/patch.c
@@ -292,7 +292,14 @@ main(int argc, char *argv[])

                /* for ed script just up and do it and exit */
                if (diff_type == ED_DIFF) {
-                       do_ed_script();
+                       if (!skip_rest_of_patch)
+                               do_ed_script();
+                       if (ofp)
+                               fclose(ofp);
+                       ofp = NULL;
+                       if (rejfp)
+                               fclose(rejfp);
+                       rejfp = NULL;
                        continue;
                }

Index: usr.bin/patch/pch.c
===================================================================
RCS file: /cvs/src/usr.bin/patch/pch.c,v
retrieving revision 1.66
diff -u -p -r1.66 pch.c
--- usr.bin/patch/pch.c 12 Jul 2023 15:45:34 -0000      1.66
+++ usr.bin/patch/pch.c
@@ -180,6 +180,8 @@ static char *posix_name(const struct fil
 bool
 there_is_another_patch(void)
 {
+       static off_t prev_p_start;
+       static int prev_diff_type;
        bool exists = false;

        if (p_base != 0 && p_base >= p_filesize) {
@@ -190,6 +192,14 @@ there_is_another_patch(void)
        if (verbose)
                say("Hmm...");
        diff_type = intuit_diff_type();
+       if (diff_type && p_start == prev_p_start &&
+           diff_type == prev_diff_type) {
+               if (verbose)
+                       say("  Ignoring the trailing garbage.\ndone\n");
+               return false;
+       }
+       prev_p_start = p_start;
+       prev_diff_type = diff_type;
        if (!diff_type) {
                if (p_base != 0) {
                        if (verbose)

Reply via email to