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)