From 58b2547c6d1f6bd94e0f85efa4bb4979ee27eab6 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 17 Oct 2025 14:46:14 -0700 Subject: [PATCH 01/28] Make patch arguments more extensible in apply_patch() It's difficult to conditionally add additional arguments to the patch execution in apply_patch() because they are placed within a compound literal array. Make the arguments more extensible by creating a local array and an index variable to place the next argument into the array. This way, it's much easier to change the number of arguments provided at runtime. --- src/interdiff.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index d1cc9e25..513a390e 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -936,6 +936,9 @@ output_patch1_only (FILE *p1, FILE *out, int not_reverted) static int apply_patch (FILE *patch, const char *file, int reverted) { +#define MAX_PATCH_ARGS 4 + const char *argv[MAX_PATCH_ARGS]; + int argc = 0; const char *basename; unsigned long orig_lines, new_lines; size_t linelen; @@ -959,10 +962,14 @@ apply_patch (FILE *patch, const char *file, int reverted) } } - w = xpipe(PATCH, &child, "w", (char **) (const char *[]) { PATCH, - reverted ? (has_ignore_all_space ? "-Rlsp0" : "-Rsp0") - : (has_ignore_all_space ? "-lsp0" : "-sp0"), - file, NULL }); + /* Add up to MAX_PATCH_ARGS arguments for the patch execution */ + argv[argc++] = PATCH; + argv[argc++] = reverted ? (has_ignore_all_space ? "-Rlsp0" : "-Rsp0") + : (has_ignore_all_space ? "-lsp0" : "-sp0"); + argv[argc++] = file; + argv[argc++] = NULL; + + w = xpipe(PATCH, &child, "w", (char **) argv); fprintf (w, "--- %s\n+++ %s\n", basename, basename); line = NULL; From 97296d18f5bea04df52ffc0928dc9d5aff9c7864 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 30 Oct 2025 17:25:23 -0700 Subject: [PATCH 02/28] Simplify original file creation in output_delta() Remove the superfluous fseeks and simplify the original file creation process by moving relevant fseeks to come right after the file cursor was last modified. --- src/interdiff.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 513a390e..3f9e837c 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1241,19 +1241,16 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fseek (p1, pos1, SEEK_SET); fseek (p2, pos2, SEEK_SET); create_orig (p2, &file, 0, NULL); - fseek (p1, pos1, SEEK_SET); - fseek (p2, pos2, SEEK_SET); create_orig (p1, &file2, mode == mode_combine, NULL); - merge_lines(&file, &file2); pos1 = ftell (p1); + fseek (p1, start1, SEEK_SET); + fseek (p2, start2, SEEK_SET); + merge_lines(&file, &file2); /* Write it out. */ write_file (&file, tmpp1fd); write_file (&file, tmpp2fd); - fseek (p1, start1, SEEK_SET); - fseek (p2, start2, SEEK_SET); - if (apply_patch (p1, tmpp1, mode == mode_combine)) error (EXIT_FAILURE, 0, "Error applying patch1 to reconstructed file"); From 1a5dd858dbd13df1ca22549c4c37bd6d00540eb6 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 13 Nov 2025 17:36:12 -0800 Subject: [PATCH 03/28] Exclude newline character from colorized output Coloring the newline character results in the terminal cursor becoming colored when the final line in the interdiff is colored. Fix this by not coloring the newline character. --- src/interdiff.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interdiff.c b/src/interdiff.c index 3f9e837c..460c1a90 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1143,7 +1143,8 @@ trim_context (FILE *f /* positioned at start of @@ line */, fwrite (line, (size_t) got, 1, out); continue; } - print_color (out, type, "%s", line); + print_color (out, type, "%.*s", (int) got - 1, line); + fputc ('\n', out); } } From 3a6e39da1ab8f31ebd8e61abf617aec6b9fb508b Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 14 Nov 2025 09:19:49 -0800 Subject: [PATCH 04/28] Fix content skipping for patch2 in index_patch_generic() When an @@ line isn't immediately after the +++ line in patch2, the next line is checked from the top of the loop which tries to search for a +++ line again, even though the +++ was already found. This results in the +++ not being found again and thus a spurious error that patch2 is empty. Fix this by making the patch2 case loop over the next line until either an @@ is found or the patch is exhausted. --- src/interdiff.c | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 460c1a90..b4578f8b 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1447,29 +1447,36 @@ index_patch_generic (FILE *patch_file, struct file_list **file_list, int need_sk /* For patch2, we need to handle the @@ line and skip content */ if (need_skip_content) { - if (getline (&line, &linelen, patch_file) == -1) { + int found = 0; + + while (!found && + getline (&line, &linelen, patch_file) > 0) { + if (strncmp (line, "@@ ", 3)) + continue; + + p = strchr (line + 3, '+'); + if (!p) + continue; + + p = strchr (p, ','); + if (p) { + /* Like '@@ -1,3 +1,3 @@' */ + p++; + skip = strtoul (p, &end, 10); + if (p == end) + continue; + } else + /* Like '@@ -1 +1 @@' */ + skip = 1; + found = 1; + } + + if (!found) { free (names[0]); free (names[1]); break; } - if (strncmp (line, "@@ ", 3)) - goto try_next; - - p = strchr (line + 3, '+'); - if (!p) - goto try_next; - p = strchr (p, ','); - if (p) { - /* Like '@@ -1,3 +1,3 @@' */ - p++; - skip = strtoul (p, &end, 10); - if (p == end) - goto try_next; - } else - /* Like '@@ -1 +1 @@' */ - skip = 1; - add_to_list (file_list, best_name (2, names), pos); while (skip--) { @@ -1485,7 +1492,6 @@ index_patch_generic (FILE *patch_file, struct file_list **file_list, int need_sk add_to_list (file_list, best_name (2, names), pos); } - try_next: free (names[0]); free (names[1]); } From a196af68e73b7e6bde8d84b3ce69a46c05a07519 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:35:28 -0800 Subject: [PATCH 05/28] Implement an advanced fuzzy diffing feature for interdiff This implements a --fuzzy option to make interdiff perform a fuzzy comparison between two diffs. This is very helpful, for example, for comparing a backport patch to its upstream source patch to assist a human reviewer in verifying the correctness of the backport. The fuzzy diffing process is complex and works by: - Generating a new patch file with hunks split up into smaller hunks to separate out multiple deltas (+/- lines) in a single hunk that are spaced apart by context lines, increasing the amount of deltas that can be applied successfully with fuzz - Applying the rewritten p1 patch to p2's original file, and the rewritten p2 patch to p1's original file; the original files aren't ever merged - Relocating patched hunks in only p1's original file to align with their respective locations in the other file, based on the reported line offset printed out by `patch` for each hunk it successfully applied - Squashing unline gaps fewer than max_context*2 lines between hunks in the patched files, to hide unknown contextual information that is irrelevant for comparing the two diffs while also improving hunk alignment between the two patched files - Diffing the two patched files as usual - Rewriting the hunks in the diff output to exclude unlines from the unified diff, even splitting up hunks to remove unlines present in the middle of a hunk, while also adjusting the @@ line to compensate for the change in line offsets - Emitting the rewritten diff output while interleaving rejected hunks from both p1 and p2 in the output in order by line number, with a comment on the @@ line indicating when an emitted hunk is a rejected hunk This also involves working around some bugs in `patch` itself encountered along the way, such as occasionally inaccurate line offsets printed out and spurious fuzzing in certain cases that involve hunks with an unequal number of pre-context and post-context lines. The end result of all of this is a minimal set of real differences in the context lines of each hunk between the user's provided diffs. Even when fuzzing results in a faulty patch, the context differences are shown so there is never a risk of any real deltas getting hidden due to fuzzing. By default, the fuzz factor used is just the default used in `patch`. The fuzz factor can be adjusted by the user via appending =N to `--fuzzy` to specify the maximum number of context lines for `patch` to fuzz. --- src/interdiff.c | 1101 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1071 insertions(+), 30 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index b4578f8b..494ac7c4 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -37,6 +37,7 @@ #include #include #include +#include #ifdef HAVE_UNISTD_H # include #endif /* HAVE_UNISTD_H */ @@ -47,6 +48,8 @@ #ifdef HAVE_SYS_WAIT_H # include #endif /* HAVE_SYS_WAIT_H */ +#include +#include #include "util.h" #include "diff.h" @@ -110,6 +113,38 @@ struct lines_info { struct lines *tail; }; +struct hunk_info { + char *s; /* Start of hunk */ + size_t len; /* Length of hunk in bytes */ + unsigned long nstart; /* Starting line number */ + unsigned long nend; /* Ending line number (inclusive) */ + int relocated:1, /* Whether or not this hunk was relocated */ + discard:1; /* Whether or not to discard this hunk */ +}; + +struct hunk_reloc { + unsigned long new; /* New starting line number */ + long off; /* Offset from the old starting line number */ + unsigned long fuzz; /* Fuzz amount reported by patch */ + int ignored:1; /* Whether or not this relocation was ignored */ +}; + +struct line_info { + char *s; /* Start of line */ + size_t len; /* Length of line in bytes */ +}; + +struct xtra_context { + unsigned long num; /* Number of extra context lines */ + char *s; /* String of extra context lines */ + size_t len; /* Length of extra context string in bytes */ +}; + +struct rej_file { + FILE *fp; + unsigned long off; +}; + static int human_readable = 1; static char *diff_opts[100]; static int num_diff_opts = 0; @@ -122,6 +157,8 @@ static int no_revert_omitted = 0; static int use_colors = 0; static int color_option_specified = 0; static int debug = 0; +static int fuzzy = 0; +static int max_fuzz_user = -1; static struct patlist *pat_drop_context = NULL; @@ -934,18 +971,19 @@ output_patch1_only (FILE *p1, FILE *out, int not_reverted) } static int -apply_patch (FILE *patch, const char *file, int reverted) +apply_patch (FILE *patch, const char *file, int reverted, FILE **out) { -#define MAX_PATCH_ARGS 4 +#define MAX_PATCH_ARGS 9 const char *argv[MAX_PATCH_ARGS]; int argc = 0; const char *basename; unsigned long orig_lines, new_lines; + char *line, *fuzz_arg = NULL; size_t linelen; - char *line; + int fildes[4]; + FILE *r, *w; pid_t child; int status; - FILE *w; basename = strrchr (file, '/'); if (basename) @@ -964,12 +1002,68 @@ apply_patch (FILE *patch, const char *file, int reverted) /* Add up to MAX_PATCH_ARGS arguments for the patch execution */ argv[argc++] = PATCH; - argv[argc++] = reverted ? (has_ignore_all_space ? "-Rlsp0" : "-Rsp0") - : (has_ignore_all_space ? "-lsp0" : "-sp0"); + argv[argc++] = reverted ? (has_ignore_all_space ? "-Rlp0" : "-Rp0") + : (has_ignore_all_space ? "-lp0" : "-p0"); + if (fuzzy) { + int fuzz = 0; + + /* Don't generate .orig files when we expect rejected hunks */ + argv[argc++] = "--no-backup-if-mismatch"; + + /* When reverting a rejected hunk, use the maximum possible + * fuzz, don't generate .rej files, and don't let patch ask to + * unreverse our hunk. Otherwise, either pass in the user- + * supplied max fuzz, or fuzz all but one pre-context and one + * post-context line by default. */ + if (reverted) { + fuzz = INT_MAX; + argv[argc++] = "--reject-file=-"; + argv[argc++] = "-N"; + } else if (max_fuzz_user >= 0) { + fuzz = max_fuzz_user; + } else if (max_context) { + fuzz = max_context - 1; + } + if (asprintf (&fuzz_arg, "--fuzz=%d", fuzz) < 0) + error (EXIT_FAILURE, errno, "asprintf failed"); + argv[argc++] = fuzz_arg; + } + /* Fuzzy mode needs hunk offset messages. Only silence output when + * piping stdout wasn't requested. */ + if (!out) + argv[argc++] = "--silent"; argv[argc++] = file; argv[argc++] = NULL; - w = xpipe(PATCH, &child, "w", (char **) argv); + /* Flush any pending writes, set up two pipes, and then fork */ + fflush (NULL); + if (pipe (fildes) == -1 || pipe (&fildes[2]) == -1) + error (EXIT_FAILURE, errno, "pipe failed"); + child = fork (); + if (child == -1) { + perror ("fork"); + exit (1); + } + + if (child == 0) { + /* Keep two pipes: one open to stdin, one to stdout */ + close (0); + close (1); + if (dup (fildes[0]) == -1 || dup (fildes[3]) == -1) + error (EXIT_FAILURE, errno, "dup failed"); + close (fildes[0]); + close (fildes[1]); + close (fildes[2]); + close (fildes[3]); + execvp (argv[0], (char **)argv); + } + free (fuzz_arg); + + /* Open the read and write ends of the two pipes */ + if (!(r = fdopen (fildes[2], "r")) || !(w = fdopen (fildes[1], "w"))) + error (EXIT_FAILURE, errno, "fdopen"); + close (fildes[0]); + close (fildes[3]); fprintf (w, "--- %s\n+++ %s\n", basename, basename); line = NULL; @@ -1006,6 +1100,12 @@ apply_patch (FILE *patch, const char *file, int reverted) fclose (w); waitpid (child, &status, 0); + /* Provide the output from patch if requested */ + if (out) + *out = r; + else + fclose (r); + if (line) free (line); @@ -1031,9 +1131,12 @@ trim_context (FILE *f /* positioned at start of @@ line */, unsigned long orig_count, orig_orig_count, new_orig_count; unsigned long new_count, orig_new_count, new_new_count; unsigned long total_count = 0; + char *atat_comment; + ssize_t got; /* Read @@ line. */ - if (getline (&line, &linelen, f) < 0) + got = getline (&line, &linelen, f); + if (got < 0) break; if (line[0] == '\\') { @@ -1043,10 +1146,16 @@ trim_context (FILE *f /* positioned at start of @@ line */, } if (read_atatline (line, &orig_offset, &orig_count, - &new_offset, &new_count)) + &new_offset, &new_count) || + !(atat_comment = strstr (line + 1, "@@"))) error (EXIT_FAILURE, 0, "Line not understood: %s", line); + /* Check if there's a comment after the @@ line to retain */ + if (atat_comment + 3 - line < got) + atat_comment = xstrdup (atat_comment + 2); + else + atat_comment = NULL; orig_orig_count = new_orig_count = orig_count; orig_new_count = new_new_count = new_count; fgetpos (f, &pos); @@ -1107,18 +1216,25 @@ trim_context (FILE *f /* positioned at start of @@ line */, fsetpos (f, &pos); if (new_orig_count != 1 && new_new_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu,%lu @@\n", + print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu,%lu @@", orig_offset, new_orig_count, new_offset, new_new_count); else if (new_orig_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu @@\n", + print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu @@", orig_offset, new_orig_count, new_offset); else if (new_new_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu +%lu,%lu @@\n", + print_color (out, LINE_HUNK, "@@ -%lu +%lu,%lu @@", orig_offset, new_offset, new_new_count); else - print_color (out, LINE_HUNK, "@@ -%lu +%lu @@\n", + print_color (out, LINE_HUNK, "@@ -%lu +%lu @@", orig_offset, new_offset); + if (atat_comment) { + fputs (atat_comment, out); + free (atat_comment); + } else { + fputc ('\n', out); + } + while (total_count--) { enum line_type type; ssize_t got = getline (&line, &linelen, f); @@ -1157,17 +1273,859 @@ trim_context (FILE *f /* positioned at start of @@ line */, return 0; } +static void +output_rej_hunks (const char *diff, struct rej_file **rej1, + struct rej_file **rej2, FILE *out) +{ + char *line = NULL; + + while (*rej1 || *rej2) { + struct rej_file **rej_ptr = rej1, *rej; + int first_line_done = 0, patch_id = 1; + unsigned long diff_off; + long next_atat_pos; + size_t linelen; + ssize_t got; + + /* Pick the reject hunk that comes first */ + if (!*rej1 || (*rej2 && (*rej2)->off < (*rej1)->off)) { + rej_ptr = rej2; + patch_id = 2; + } + rej = *rej_ptr; + + if (diff) { + /* Wait until the current diff line is an @@ line */ + if (strncmp (diff, "@@ ", 3)) + return; + + if (read_atatline (diff, &diff_off, NULL, NULL, NULL)) + error (EXIT_FAILURE, 0, "line not understood: %s", + diff); + + /* Stop if the diff hunk comes next */ + if (rej->off > diff_off) + return; + } + + /* Write the rej hunk until EOF or the next @@ line (i.e., next + * hunk). Note that rej starts at the current @@ line that we + * must write, so don't look for the next @@ until after the + * first line is written. */ + for (;;) { + got = getline (&line, &linelen, rej->fp); + if (got <= 0) { + if (feof (rej->fp)) + goto rej_file_eof; + error (EXIT_FAILURE, errno, + "Failed to read line from .rej"); + } + if (first_line_done) { + if (!strncmp (line, "@@ ", 3)) + break; + + fwrite (line, (size_t) got, 1, out); + next_atat_pos = ftell (rej->fp); + } else { + /* Append a comment after the @@ line indicating + * this is a rejected hunk. */ + first_line_done = 1; + fwrite (line, (size_t) got - 1, 1, out); + fprintf (out, " INTERDIFF: rejected hunk from patch%d, cannot diff context\n", + patch_id); + } + } + + /* Record the line offset of the next rej hunk, if any */ + if (read_atatline (line, &rej->off, NULL, NULL, NULL)) + error (EXIT_FAILURE, 0, "line not understood: %s", line); + fseek (rej->fp, next_atat_pos, SEEK_SET); + + if (!feof (rej->fp)) + continue; + +rej_file_eof: + /* Clear out this reject file pointer when it's finished */ + *rej_ptr = NULL; + } + + free (line); +} + +/* `xctx` must come with `num` initialized and `s` and `len` zeroed */ +static void +ctx_lookbehind (const struct line_info *lines, unsigned long start_line_idx, + struct xtra_context *xctx) +{ + unsigned long i, num = 0; + + for (i = start_line_idx - 1; i < start_line_idx; i--) { + const struct line_info *line = &lines[i]; + + if (*line->s == '+') + continue; + + /* Copy out the line and ensure the first character is a space, + * since it may be a minus. */ + xctx->s = xrealloc (xctx->s, xctx->len + line->len); + memmove (xctx->s + line->len, xctx->s, xctx->len); + memcpy (xctx->s, line->s, line->len); + *xctx->s = ' '; + xctx->len += line->len; + + /* Quit when we've got the desired number of context lines */ + if (++num == xctx->num) + return; + } + + /* Record the actual number of extra content lines found, since it is + * less than the number of lines requested. */ + xctx->num = num; +} + +/* `xctx` must come with `num` initialized and `s` and `len` zeroed */ +static void +ctx_lookahead (const char *hunk, size_t hlen, struct xtra_context *xctx) +{ + const char *line, *next_line; + unsigned long num = 0; + size_t linelen; + + /* `hunk` is positioned at the first character of the current line + * parsed by split_patch_hunks(). Reduce it by one first to go to the + * newline character of the previous line, to make our loop simpler. */ + for (line = hunk - 1;; line = next_line) { + /* Go to the character _after_ the newline character */ + line++; + + /* Get the next line now to find the length of the line */ + next_line = memchr (line, '\n', hunk + hlen - line); + if (*line == '+') + continue; + + linelen = next_line + 1 - line; + + /* Copy out the line and ensure the first character is a space, + * since it may be a minus. */ + xctx->s = xrealloc (xctx->s, xctx->len + linelen); + memcpy (xctx->s + xctx->len, line, linelen); + xctx->s[xctx->len] = ' '; + xctx->len += linelen; + + /* Quit when we've got the desired number of context lines */ + if (++num == xctx->num) + break; + + /* Stop when this is the end of the hunk, recording the actual + * number of extra context lines found. */ + if (!next_line || next_line + 1 == hunk + hlen) { + xctx->num = num; + break; + } + } +} + +/* Squash up to max_context*2 unlines between two hunks */ +static int +squash_unline_gap (char **line_ptr, size_t hlen, const char *unline, + size_t unline_len) +{ + char *hunk = *line_ptr, *line = hunk, *prev = line; + unsigned int num_unlines = 1; + int squash = 0; + + for (; (line = memchr (line, '\n', hunk + hlen - line)); prev = line) { + /* Go to the character _after_ the newline character */ + line++; + + /* Stop when there's nothing left */ + if (line == hunk + hlen) + break; + + /* Move the line pointer to the last unline in the chunk of up + * to max_context*2 unlines so the loop in split_patch_hunks() + * skips over it and thus skips over the entire unline chunk. */ + if (strncmp (line + 1, unline, unline_len)) { + squash = 1; + break; + } + + if (++num_unlines > max_context * 2) + break; + } + + /* Always advance the line pointer even without squashing */ + *line_ptr = prev; + return squash; +} + +static void +write_xctx (struct xtra_context *xctx, FILE *out) +{ + if (xctx->s) { + fwrite (xctx->s, xctx->len, 1, out); + free (xctx->s); + } +} + +/* Regenerate a patch with the hunks split up to ensure more of the patch gets + * applied successfully. Outputs a `hunk_offs` array (if requested) to map each + * hunk's post-split offset from the original hunk's new line number. + * + * When the unline is provided, that is a hint to strip unlines from context and + * perform splits at unlines in the middle of a hunk. */ +static FILE * +split_patch_hunks (FILE *patch, size_t len, char *file, + unsigned long **hunk_offs, const char *unline) +{ + char *fbuf, *hunk, *next_hunk; + unsigned long hnum = 0; + int has_output = 0; + size_t unline_len; + FILE *out; + + /* Read the patch into a NUL-terminated buffer */ + if (len) { + fbuf = xmalloc (len + 1); + if (fread (fbuf, 1, len, patch) != len) + error (EXIT_FAILURE, errno, "fread() of patch failed"); + } else { + /* The patch is a pipe; we can't seek it, so read until EOF */ + fbuf = NULL; + for (int ch; (ch = fgetc (patch)) != EOF;) { + fbuf = xrealloc (fbuf, ++len + 1); + fbuf[len - 1] = ch; + } + fclose (patch); + } + fbuf[len] = '\0'; + + /* Find the first hunk. `fbuf` is positioned at the start of a line. */ + if (!strncmp (fbuf, "@@ ", 3)) { + hunk = fbuf; + } else { + hunk = strstr (fbuf, "\n@@ "); + if (!hunk) + error (EXIT_FAILURE, 0, "patch file malformed: %s", fbuf); + } + + if (unline) { + /* Create a temporary file for the unline-cleansed output */ + out = xtmpfile (); + + /* Find the length of the unline now to use it in the loop */ + unline_len = strlen (unline); + } else { + /* Create the output file by temporarily modifying `file` */ + strcat (file, ".patch"); + out = xopen (file, "w+"); + file[strlen (file) - strlen (".patch")] = '\0'; + } + + do { + /* nctx[0] = pre-context lines, nctx[1] = post-context lines + * ndelta[0] = deleted lines, ndelta[1] = added lines */ + unsigned long nctx[2] = {}, ndelta[2] = {}, nctx_target; + unsigned long ostart, nstart, orig_nstart, start_line_idx = 0; + struct xtra_context xctx_pre = {}; + struct line_info *lines = NULL; + unsigned long num_lines = 0; + int skipped_lines = 0; + char *line; + size_t hlen; + + if (read_atatline (hunk, &ostart, NULL, &nstart, NULL)) + error (EXIT_FAILURE, 0, "line not understood: %s", + strsep (&hunk, "\n")); + + /* Save the original hunk's new line number */ + orig_nstart = nstart; + + /* Find the next hunk now to tell where the current hunk ends */ + next_hunk = strstr (hunk, "\n@@ "); + if (next_hunk) + hlen = ++next_hunk - hunk; + else + hlen = strlen (hunk); + + /* Count the number of pre-context and post-context lines in + * this hunk. The greater of the two will be the number of pre- + * context and post-context lines targeted per split hunk. */ + if (!unline) { + unsigned long orig_hunk_nctx[2] = {}; + + for (line = hunk; + (line = memchr (line, '\n', hunk + hlen - line)) && + line[1] == ' '; line++, orig_hunk_nctx[0]++); + for (line = hunk + hlen - 1; + (line = memrchr (hunk, '\n', line - hunk)) && + line[1] == ' '; line--, orig_hunk_nctx[1]++); + nctx_target = MAX (orig_hunk_nctx[0], orig_hunk_nctx[1]); + } + + /* Split this hunk into multiple smaller hunks, if possible. + * This is done by looking for deltas (+/- lines) that aren't + * contiguous and thus have context lines in between them. Note + * that the first line is intentionally skipped because the + * first line is the @@ line. When no splitting occurs, this + * still has the effect of trimming context lines for the hunk + * to ensure the number of pre-context lines and post-context + * lines are equal. */ + for (line = hunk; (line = memchr (line, '\n', hunk + hlen - line));) { + unsigned long start_off = 0, onum, nnum; + struct line_info *start_line, *end_line; + struct xtra_context xctx_post = {}; + size_t hlen_rem; + + /* Go to the character _after_ the newline character */ + line++; + + /* Set the length of the previous line (if any). Only do + * this once because when doing unline splitting, the + * unlines aren't recorded into the lines array. */ + if (lines && !lines[num_lines - 1].len) + lines[num_lines - 1].len = + line - lines[num_lines - 1].s; + + /* Check if this is the end. If so, terminate the hunk + * now because there isn't any new line to parse. */ + hlen_rem = hunk + hlen - line; + if (!hlen_rem) + goto split_hunk_incl_latest; + + /* Check if this is an unline that we need to remove */ + if (unline && !strncmp (line + 1, unline, unline_len)) { + /* Split the hunk now if there's a delta, unless + * this is a bogus hunk from a rejected patch + * hunk. Bogus hunks stem from one side of the + * diff operation consisting only of unlines. + * Such diffs have only unlines in their context + * and only one delta type: either additions or + * subtractions, _not_ both. Discard bogus hunks + * by skipping over them here, which is fine + * since the corresponding rejected patch hunk + * is emitted later. + * + * Sometimes a hunk may appear bogus when it is + * not; this can be identified by checking if + * there are no more than max_context*2 unlines + * until the next hunk. Squash the unlines away + * in that case, which alters the line numbers + * of the hunk as a side effect. The assumption + * is that these two hunks are related to each + * other but are just slightly offset in the two + * diffed files due to small bits of missing + * context that were filled in with unlines. */ + if (ndelta[0] || ndelta[1]) { + if (nctx[0] || nctx[1] || + (ndelta[0] && ndelta[1])) + goto split_hunk_incl_latest; + + if (squash_unline_gap (&line, hlen_rem, + unline, + unline_len)) { + skipped_lines = 1; + continue; + } + } + + /* Move forward the starting line offset, + * discarding any pre-context lines seen. The + * starting line index is set to the _next_ + * (non-unline) line, which may not exist. */ + start_line_idx = num_lines; + start_off += nctx[0] + 1; + nctx[0] = 0; + continue; + } + + /* Record the current line, setting `len` to zero */ + lines = xrealloc (lines, ++num_lines * sizeof (*lines)); + lines[num_lines - 1] = (typeof (*lines)){ line }; + + /* Track +/- lines as well as pre-context and post- + * context lines. Split the hunk upon encountering a +/- + * line after post-context lines, unless we're splitting + * at unlines instead. */ + if (*line == '+' || *line == '-') { + if (!unline && nctx[1]) { + /* The current line belongs to the + * _next_ split hunk. Exclude it. */ + end_line = &lines[num_lines - 2]; + goto split_hunk; + } + + ndelta[*line == '+']++; + } else { + nctx[ndelta[0] || ndelta[1]]++; + } + + /* Keep parsing until there's a need to do a split */ + continue; + +split_hunk_incl_latest: + /* Split the hunk including the latest recorded line */ + end_line = &lines[num_lines - 1]; +split_hunk: + /* Stop now if there are no lines left to make a hunk */ + if (start_line_idx == num_lines) + break; + + /* Check that there's an actual delta recorded */ + if (!ndelta[0] && !ndelta[1]) + error (EXIT_FAILURE, 0, "hunk without +/- lines?"); + + /* Split the current hunk by terminating it and starting + * a new hunk. When generating a patch to apply, there + * must be the same number of pre-context lines as post- + * context lines, otherwise patch will need to fuzz the + * extra context lines. An exception is when the context + * is at either the beginning or end of the file. Target + * having the same number of pre-context and post- + * context lines as the original hunk itself, so the + * user-provided fuzz factor behaves as expected. Note + * that this adjustment impacts ostart and nstart either + * for the current split hunk or the next split hunk. */ + start_line = &lines[start_line_idx]; + if (unline) { + /* Add the start offset to the old/new lines */ + ostart += start_off; + nstart += start_off; + } else if (nctx[1] < nctx_target && hlen_rem) { + /* If the number of post-context lines is still + * below the target number afterwards, then it + * means we hit the end of the original hunk + * itself. It's technically fine because it + * means the original hunk came with an unequal + * number of pre- and post-context lines. */ + xctx_post.num = nctx_target - nctx[1]; + ctx_lookahead (line, hlen_rem, &xctx_post); + } + + /* Calculate the old and new line counts */ + onum = nnum = xctx_pre.num + /* Extra pre-context */ + end_line + 1 - start_line + /* Hunk */ + xctx_post.num; /* Extra post-context */ + onum -= ndelta[1]; + nnum -= ndelta[0]; + + /* Emit the hunk to the output file */ + fprintf (out, "@@ -%lu,%lu +%lu,%lu @@\n", + ostart, onum, nstart, nnum); + write_xctx (&xctx_pre, out); + /* If lines were skipped, then the output needs to be + * written one line at a time. */ + if (skipped_lines) { + skipped_lines = 0; + for (unsigned long i = start_line_idx; + &lines[i] <= end_line; i++) + fwrite (lines[i].s, lines[i].len, 1, out); + } else { + fwrite (start_line->s, + end_line->s + end_line->len - start_line->s, + 1, out); + } + write_xctx (&xctx_post, out); + has_output = 1; + + /* Save the offset from this hunk's original new line */ + if (hunk_offs) { + *hunk_offs = xrealloc (*hunk_offs, ++hnum * + sizeof (*hunk_offs)); + (*hunk_offs)[hnum - 1] = nstart - orig_nstart; + } + + /* Stop when there's nothing left */ + if (!hlen_rem) + break; + + /* Start the next hunk */ + start_line_idx = num_lines; + ostart += onum; + nstart += nnum; + if (unline) { + /* The current line is not included in the next + * hunk when splitting at unlines. */ + nctx[0] = nctx[1] = ndelta[0] = ndelta[1] = 0; + } else { + /* Find extra pre-context if extra post-context + * was used for this split hunk, since it means + * that there isn't enough normal post-context + * to be the next split hunk's pre-context. */ + start_line_idx -= 1 + nctx[1]; + xctx_pre = (typeof (xctx_pre)){ xctx_post.num }; + if (xctx_pre.num) + ctx_lookbehind (lines, start_line_idx, + &xctx_pre); + + /* Subtract the extra post-context lines of this + * hunk, the normal post-context lines of this + * hunk, and the extra pre-context lines for the + * _next_ hunk to get the _next_ hunk's starting + * line numbers. */ + ostart -= xctx_pre.num + xctx_post.num + nctx[1]; + nstart -= xctx_pre.num + xctx_post.num + nctx[1]; + nctx[0] = nctx[1]; + nctx[1] = 0; + ndelta[1] = *line == '+'; + ndelta[0] = !ndelta[1]; + } + } + free (lines); + } while ((hunk = next_hunk)); + free (fbuf); + + /* No output, no party. Can happen if the hunks were only unlines. */ + if (!has_output) { + fclose (out); + return NULL; + } + + /* Reposition the output file back to the beginning */ + rewind (out); + return out; +} + +static int +hunk_info_cmp (const void *lhs_ptr, const void *rhs_ptr) +{ + const struct hunk_info *lhs = lhs_ptr, *rhs = rhs_ptr; + + return lhs->nstart - rhs->nstart; +} + +static int +hunk_reloc_cmp (const void *lhs_ptr, const void *rhs_ptr) +{ + const struct hunk_reloc *lhs = lhs_ptr, *rhs = rhs_ptr; + + return lhs->new - rhs->new; +} + +static void +parse_fuzzed_hunks (FILE *patch_out, const unsigned long *hunk_offs, + struct hunk_reloc **relocs, unsigned long *num_relocs) +{ + char *line = NULL; + size_t linelen; + + /* Parse out each fuzzed hunk's line offset */ + while (getline (&line, &linelen, patch_out) > 0) { + struct hunk_reloc *prev = &(*relocs)[*num_relocs - 1]; + unsigned long fuzz = 0, hnum, lnum; + long off; + + if (sscanf (line, "Hunk #%lu succeeded at %lu (offset %ld", + &hnum, &lnum, &off) != 3 && + sscanf (line, "Hunk #%lu succeeded at %lu with fuzz %lu (offset %ld", + &hnum, &lnum, &fuzz, &off) != 4) + continue; + + /* Recover the correct new line number of the possibly-split + * hunk, and skip it if it matches the relocated new line number + * of the previous hunk (if any). Split hunks are contiguous. */ + lnum -= hunk_offs[hnum - 1]; + if (*relocs && lnum - off == prev->new - prev->off) + continue; + + *relocs = xrealloc (*relocs, ++*num_relocs * sizeof (**relocs)); + (*relocs)[*num_relocs - 1] = + (typeof (**relocs)){ lnum, off, fuzz }; + } + free (line); +} + +static void +fuzzy_relocate_hunks (const char *file, const char *unline, FILE *patch_out, + const unsigned long *hunk_offs) +{ + struct hunk_info *hunks = NULL; + struct hunk_reloc *relocs = NULL; + unsigned long num_hunks = 0, num_relocs = 0; + unsigned long i, j, num_unlines = 0; + char *end, *endl, *fbuf, *start; + int new_hunk = 1; + size_t unlinelen; + struct stat st; + FILE *fp; + + /* Parse the fuzzed hunks when relocating for line offset differences */ + if (patch_out) + parse_fuzzed_hunks (patch_out, hunk_offs, &relocs, &num_relocs); + + /* Open the patched file and copy it into a buffer */ + if (stat (file, &st) < 0) + error (EXIT_FAILURE, errno, "stat() fail"); + fbuf = xmalloc (st.st_size); + fp = xopen (file, "r"); + if (fread (fbuf, 1, st.st_size, fp) != st.st_size) + error (EXIT_FAILURE, errno, "fread() fail"); + fclose (fp); + + /* Sort the relocations array by ascending order of new line number. A + * relocation may indicate that a contiguous block of code should + * actually be split into two or more hunks to better align with the + * other file, since they are split up in the other file. Sorting the + * relocations is needed for tracking this during hunk enumeration. */ + if (relocs) + qsort (relocs, num_relocs, sizeof (*relocs), hunk_reloc_cmp); + + /* Enumerate every hunk in the file */ + start = fbuf; /* Start of the line */ + end = fbuf + st.st_size; /* End of the file */ + unlinelen = strlen(unline); /* Unline length (includes newline char) */ + for (endl = fbuf, i = 1, j = 0; + (endl = memchr (endl, '\n', end - endl)); + start = ++endl, i++) { + size_t len = endl - start + 1; + + /* Cut a new hunk if a relocated hunk starts at this line. This + * is important because a relocated hunk may start in the middle + * of a larger hunk, which is a hint to split the hunk. Note + * that a relocation may occur on an unline, which is corrected + * later on in a different loop. When that is the case, we still + * need to iterate past the relocation at that line in order to + * continue through the relocations array. */ + if (j < num_relocs && i == relocs[j].new) { + j++; + new_hunk = 1; + } + + /* Skip over unlines */ + if (len == unlinelen && !memcmp (start, unline, len)) { + num_unlines++; + new_hunk = 1; + continue; + } + + /* Keep expanding the current detected hunk */ + if (!new_hunk) { + hunks[num_hunks - 1].len += len; + hunks[num_hunks - 1].nend++; + num_unlines = 0; + continue; + } + new_hunk = 0; + + /* Start a new hunk */ + hunks = xrealloc (hunks, ++num_hunks * sizeof (*hunks)); + hunks[num_hunks - 1] = (typeof (*hunks)){ start, len, i, i }; + + /* Check the number of unlines between the end of the previous + * hunk (if any) and the start of the current hunk. If there are + * no more than max_context*2 unlines between the two, then eat + * the unlines and combine the hunks together. Note that we must + * also ignore the relocation for this hunk, if any, while + * accounting for the relocation new line possibly being up to + * `fuzz` lines _before_ the actual line (see more below). */ + if (num_hunks > 1 && num_unlines <= max_context * 2) { + struct hunk_info *hcurr = &hunks[num_hunks - 1]; + struct hunk_info *hprev = hcurr - 1; + + for (int k = num_relocs - 1; k >= 0; k--) { + struct hunk_reloc *rcurr = &relocs[k]; + unsigned long delta; + + if (rcurr->new <= hcurr->nstart) { + delta = hcurr->nstart - rcurr->new; + if (delta <= rcurr->fuzz) + rcurr->ignored = 1; + break; + } + } + + hcurr->nstart = hcurr->nend = hprev->nend + 1; + } + num_unlines = 0; + } + + /* Check and possibly correct the new line number in the case of fuzzed + * hunks. Patch can screw this up and emit a line number up to `fuzz` + * lines _before_ the actual line. */ + for (i = 0; i < num_relocs; i++) { + struct hunk_reloc *rcurr = &relocs[i]; + + /* Skip ignored relocations and relocations without fuzz */ + if (rcurr->ignored || !rcurr->fuzz) + continue; + + for (j = 0; j < num_hunks; j++) { + struct hunk_info *hcurr = &hunks[j]; + unsigned long delta; + + /* Find a hunk that starts within `fuzz` lines after + * this relocation. If it does, correct the new line + * number and the offset to use this hunk. */ + if (hcurr->nstart >= rcurr->new) { + delta = hcurr->nstart - rcurr->new; + if (delta <= rcurr->fuzz) { + rcurr->new += delta; + rcurr->off += delta; + } + break; + } + } + } + + /* Apply relocations */ + for (i = 0; i < num_relocs; i++) { + struct hunk_reloc *rcurr = &relocs[i]; + int found = 0; + + if (rcurr->ignored) + continue; + + for (j = 0; j < num_hunks; j++) { + struct hunk_info *hcurr = &hunks[j], *hprev = hcurr - 1; + + /* Make sure we don't relocate a hunk more than once */ + if (hcurr->relocated) + continue; + + /* Look for the hunk that starts at the new line number, + * subtracting the offset to get the hunk's _original_ + * new line number. And relocate succeeding hunks that + * had their unlines squelched between this hunk. */ + if (hcurr->nstart == rcurr->new || + (found && hcurr->nstart == + hprev->nend + rcurr->off + 1)) { + hcurr->nstart -= rcurr->off; + hcurr->nend -= rcurr->off; + hcurr->relocated = 1; + found = 1; + } else if (found) { + break; + } + } + + /* Fail if we couldn't find the hunk in question */ + if (!found) + error (EXIT_FAILURE, 0, "failed to relocate hunk"); + } + + /* Now that all hunks' final positions are determined, discard hunks + * that overlap with a relocated hunk's new position. Such hunks will + * have generated rejects on the other orig file, which will be emitted + * separately and thus removing the conflicting hunk here won't result + * in any loss of information from the diff. */ + for (i = 0; i < num_hunks; i++) { + /* Find the next relocated hunk */ + if (!hunks[i].relocated) + continue; + + /* Check all non-relocated hunks for conflicts to discard. It is + * possible for there to be more than one conflicting hunk. */ + for (j = 0; j < num_hunks; j++) { + if (hunks[j].relocated || hunks[j].discard) + continue; + + /* Check if hunks[j] starts or ends in hunks[i] */ + if ((hunks[j].nstart >= hunks[i].nstart && + hunks[j].nstart <= hunks[i].nend) || + (hunks[j].nend >= hunks[i].nstart && + hunks[j].nend <= hunks[i].nend)) + hunks[j].discard = 1; + } + } + + /* Sort the hunks by ascending order of starting line number */ + qsort (hunks, num_hunks, sizeof (*hunks), hunk_info_cmp); + + /* Write the final result to the patched file, maintaining the same + * unline. The result (in bytes, not lines) may be smaller than before + * due to some hunks getting discarded and thus replaced by unlines, so + * truncate the entire file before writing. */ + fp = xopen (file, "w+"); + for (i = 0, j = 1; i < num_hunks; i++) { + if (hunks[i].discard) + continue; + + /* Write out unlines between the previous and current hunks */ + for (; j < hunks[i].nstart; j++) + fwrite (unline, unlinelen, 1, fp); + j = hunks[i].nend + 1; + + /* Write out the hunk itself */ + fwrite (hunks[i].s, hunks[i].len, 1, fp); + } + + /* All done, clean everything up */ + fclose (fp); + free (fbuf); + free (hunks); + free (relocs); +} + +static void +fuzzy_do_rej (char *file, struct rej_file *rej, const char *other_file) +{ + char *line = NULL; + size_t linelen; + long atat_pos; + + /* Briefly modify `file` in-place to open the .rej file */ + strcat (file, ".rej"); + rej->fp = xopen (file, "r"); + file[strlen (file) - strlen (".rej")] = '\0'; + + /* Skip (the first two) lines to get to the start of the @@ line */ + do { + atat_pos = ftell (rej->fp); + if (getline (&line, &linelen, rej->fp) <= 0) + error (EXIT_FAILURE, errno, + "Failed to read line from .rej"); + } while (strncmp (line, "@@ ", 3)); + fseek (rej->fp, atat_pos, SEEK_SET); + + /* Export the line offset of the first rej hunk */ + if (read_atatline (line, &rej->off, NULL, NULL, NULL)) + error (EXIT_FAILURE, 0, "line not understood: %s", line); + free (line); + + /* Revert the rejected hunks on the _other_ file, so they're excluded + * from the 'diff' output. Otherwise, 'diff' will output the _reverse_ + * of the rejected hunks, which will muddy the final output as we will + * print out the rejected hunks themselves later anyway. */ + apply_patch (rej->fp, other_file, 1, NULL); + + /* Go back to the @@ after apply_patch() moved the file cursor */ + fseek (rej->fp, atat_pos, SEEK_SET); +} + +static void +fuzzy_cleanup (char *file, int rej) +{ + /* Modify the `file` string in-place */ + char *end = strchr (file, '\0'); + + /* Remove the .rej file if one was generated */ + if (rej) { + strcpy (end, ".rej"); + unlink (file); + } + + /* Remove the .patch file generated from splitting up the hunks */ + strcpy (end, ".patch"); + unlink (file); + + /* Terminate `file` back at where it was terminated originally */ + *end = '\0'; +} + static int output_delta (FILE *p1, FILE *p2, FILE *out) { const char *tmpdir = getenv ("TMPDIR"); unsigned int tmplen; - const char tail1[] = "/interdiff-1.XXXXXX"; - const char tail2[] = "/interdiff-2.XXXXXX"; + /* Reserve space for appending .rej and .patch at the end of tmpp1/2 */ + const char tail1[] = "/interdiff-1.XXXXXX\0patch"; + const char tail2[] = "/interdiff-2.XXXXXX\0patch"; char *tmpp1, *tmpp2; int tmpp1fd, tmpp2fd; struct lines_info file = { NULL, 0, 0, NULL, NULL }; struct lines_info file2 = { NULL, 0, 0, NULL, NULL }; + struct rej_file rej1, rej2; + int ret1 = 0, ret2 = 0; char *oldname = NULL, *newname = NULL; pid_t child; FILE *in; @@ -1244,21 +2202,63 @@ output_delta (FILE *p1, FILE *p2, FILE *out) create_orig (p2, &file, 0, NULL); create_orig (p1, &file2, mode == mode_combine, NULL); pos1 = ftell (p1); + pos2 = ftell (p2); fseek (p1, start1, SEEK_SET); fseek (p2, start2, SEEK_SET); - merge_lines(&file, &file2); /* Write it out. */ - write_file (&file, tmpp1fd); - write_file (&file, tmpp2fd); + if (fuzzy) { + /* Ensure the same unline is used for both files */ + write_file (&file, tmpp1fd); + file2.unline = xstrdup (file.unline); + write_file (&file2, tmpp2fd); + } else { + merge_lines (&file, &file2); + write_file (&file, tmpp1fd); + write_file (&file, tmpp2fd); + } - if (apply_patch (p1, tmpp1, mode == mode_combine)) - error (EXIT_FAILURE, 0, - "Error applying patch1 to reconstructed file"); + if (fuzzy) { + unsigned long *hunk_offs = NULL; + FILE *patch_out, *sp; + + /* Split the patch hunks into smaller hunks, then apply that */ + sp = split_patch_hunks (p1, pos1 - start1, tmpp1, &hunk_offs, NULL); + ret1 = apply_patch (sp, tmpp1, false, &patch_out); + fclose (sp); + + /* Relocate hunks in tmpp1 in order to make them align with the + * positions of the hunks in tmpp2. */ + fuzzy_relocate_hunks (tmpp1, file.unline, patch_out, hunk_offs); + fclose (patch_out); + free (hunk_offs); + + /* Split the patch hunks into smaller hunks, then apply that */ + sp = split_patch_hunks (p2, pos2 - start2, tmpp2, NULL, NULL); + ret2 = apply_patch (sp, tmpp2, false, NULL); + fclose (sp); + + /* For tmpp2 relocations, only eat unline gaps between hunks + * that amount to no more than max_context*2 lines. This was + * also done to tmpp1 during its relocation pass. */ + fuzzy_relocate_hunks (tmpp2, file.unline, NULL, NULL); + + /* Handle the rejected hunks. This needs to be done after both + * files are patched because it may revert a rejected hunk from + * the other file. */ + if (ret1) + fuzzy_do_rej (tmpp1, &rej1, tmpp2); + if (ret2) + fuzzy_do_rej (tmpp2, &rej2, tmpp1); + } else { + if (apply_patch (p1, tmpp1, mode == mode_combine, NULL)) + error (EXIT_FAILURE, 0, + "Error applying patch1 to reconstructed file"); - if (apply_patch (p2, tmpp2, 0)) - error (EXIT_FAILURE, 0, - "Error applying patch2 to reconstructed file"); + if (apply_patch (p2, tmpp2, 0, NULL)) + error (EXIT_FAILURE, 0, + "Error applying patch2 to reconstructed file"); + } fseek (p1, pos1, SEEK_SET); @@ -1285,17 +2285,30 @@ output_delta (FILE *p1, FILE *p2, FILE *out) break; } - if (!diff_is_empty) { + /* Rebuild the diff hunks without unlines, since fuzzy diffing shows + * context line differences that therefore may cause unlines to appear + * in the diff output. We don't want any unlines in the final output. */ + if (fuzzy && !diff_is_empty) { + in = split_patch_hunks (in, 0, NULL, NULL, file.unline); + diff_is_empty = !in; + } + + if (!diff_is_empty || ret1 || ret2) { + /* Initialize the rej pointers for output_rej_hunks() */ + struct rej_file *rej1_ptr = ret1 ? &rej1 : NULL; + struct rej_file *rej2_ptr = ret2 ? &rej2 : NULL; /* ANOTHER temporary file! This is to catch the case * where we just don't have enough context to generate * a proper interdiff. */ FILE *tmpdiff = xtmpfile (); char *line = NULL; size_t linelen; - for (;;) { + for (; !diff_is_empty;) { ssize_t got = getline (&line, &linelen, in); if (got < 0) break; + /* Output fuzzy diff reject hunks in order */ + output_rej_hunks (line, &rej1_ptr, &rej2_ptr, tmpdiff); fwrite (line, (size_t) got, 1, tmpdiff); if (*line != ' ' && !strcmp (line + 1, file.unline)) { /* Uh-oh. We're trying to output a @@ -1321,6 +2334,9 @@ output_delta (FILE *p1, FILE *p2, FILE *out) } free (line); + /* Output any remaining reject hunks */ + output_rej_hunks (NULL, &rej1_ptr, &rej2_ptr, tmpdiff); + /* First character */ if (human_readable) { char *p, *q, c, d; @@ -1347,13 +2363,18 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fclose (tmpdiff); } - fclose (in); + if (in) + fclose (in); waitpid (child, NULL, 0); if (debug) printf ("reconstructed orig1=%s orig2=%s\n", tmpp1, tmpp2); else { unlink (tmpp1); unlink (tmpp2); + if (fuzzy) { + fuzzy_cleanup (tmpp1, ret1); + fuzzy_cleanup (tmpp2, ret2); + } } free (oldname); free (newname); @@ -1366,6 +2387,10 @@ output_delta (FILE *p1, FILE *p2, FILE *out) else { unlink (tmpp1); unlink (tmpp2); + if (fuzzy) { + fuzzy_cleanup (tmpp1, ret1); + fuzzy_cleanup (tmpp2, ret2); + } } if (human_readable) fprintf (out, "%s impossible; taking evasive action\n", @@ -1820,7 +2845,7 @@ flipdiff (FILE *p1, FILE *p2, FILE *flip1, FILE *flip2) tmpfd = xmkstemp (tmpp1); write_file (&intermediate, tmpfd); fsetpos (p1, &at1); - if (apply_patch (p1, tmpp1, 1)) + if (apply_patch (p1, tmpp1, 1, NULL)) error (EXIT_FAILURE, 0, "Error reconstructing original file"); @@ -1829,7 +2854,7 @@ flipdiff (FILE *p1, FILE *p2, FILE *flip1, FILE *flip2) tmpfd = xmkstemp (tmpp3); write_file (&intermediate, tmpfd); fsetpos (p2, &at2); - if (apply_patch (p2, tmpp3, 0)) + if (apply_patch (p2, tmpp3, 0, NULL)) error (EXIT_FAILURE, 0, "Error reconstructing final file"); @@ -2231,7 +3256,12 @@ syntax (int err) " (interdiff) When a patch from patch1 is not in patch2,\n" " don't revert it\n" " --in-place (flipdiff) Write the output to the original input\n" -" files\n"; +" files\n" +" --fuzzy[=N]\n" +" (interdiff) Perform a fuzzy comparison, showing the minimal\n" +" set of differences including those in context lines.\n" +" Optionally set N to the maximum number of context lines\n" +" to fuzz (which passes '--fuzz=N' to the patch utility).\n"; fprintf (err ? stderr : stdout, syntax_str, progname, progname); exit (err); @@ -2293,6 +3323,7 @@ main (int argc, char *argv[]) {"flip", 0, 0, 1000 + 'F' }, {"no-revert-omitted", 0, 0, 1000 + 'R' }, {"in-place", 0, 0, 1000 + 'i' }, + {"fuzzy", 2, 0, 1000 + 'f' }, {"debug", 0, 0, 1000 + 'D' }, {"strip-match", 1, 0, 'p'}, {"unified", 1, 0, 'U'}, @@ -2380,6 +3411,16 @@ main (int argc, char *argv[]) syntax (1); flipdiff_inplace = 1; break; + case 1000 + 'f': + if (mode != mode_inter) + syntax (1); + if (optarg) { + max_fuzz_user = strtoul (optarg, &end, 0); + if (optarg == end) + syntax (1); + } + fuzzy = 1; + break; case 1000 + 'D': debug = 1; break; From 797ffecbf7b12e09c9dcaca9655f8ca6ed2aecaa Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Tue, 2 Dec 2025 09:21:21 -0800 Subject: [PATCH 06/28] interdiff: Fix incorrect base file direction in fuzzy mode Fuzzy interdiffs would show how to go from patch2 to patch1, the opposite of how it should be. Fix it so that the output shows how to go from patch1 to patch2. --- src/interdiff.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 494ac7c4..0a46ae39 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -2209,9 +2209,9 @@ output_delta (FILE *p1, FILE *p2, FILE *out) /* Write it out. */ if (fuzzy) { /* Ensure the same unline is used for both files */ - write_file (&file, tmpp1fd); + write_file (&file, tmpp2fd); file2.unline = xstrdup (file.unline); - write_file (&file2, tmpp2fd); + write_file (&file2, tmpp1fd); } else { merge_lines (&file, &file2); write_file (&file, tmpp1fd); @@ -2223,7 +2223,7 @@ output_delta (FILE *p1, FILE *p2, FILE *out) FILE *patch_out, *sp; /* Split the patch hunks into smaller hunks, then apply that */ - sp = split_patch_hunks (p1, pos1 - start1, tmpp1, &hunk_offs, NULL); + sp = split_patch_hunks (p2, pos2 - start2, tmpp1, &hunk_offs, NULL); ret1 = apply_patch (sp, tmpp1, false, &patch_out); fclose (sp); @@ -2234,7 +2234,7 @@ output_delta (FILE *p1, FILE *p2, FILE *out) free (hunk_offs); /* Split the patch hunks into smaller hunks, then apply that */ - sp = split_patch_hunks (p2, pos2 - start2, tmpp2, NULL, NULL); + sp = split_patch_hunks (p1, pos1 - start1, tmpp2, NULL, NULL); ret2 = apply_patch (sp, tmpp2, false, NULL); fclose (sp); From f2d0bb494f2c9c13dc043b6e0865a6ed600e5214 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Tue, 2 Dec 2025 10:39:45 -0800 Subject: [PATCH 07/28] interdiff: Fix bogus hunk removal in split_patch_hunks() split_patch_hunks() fails to filter bogus hunks that contain real context lines mixed with unlines. Such hunks are still bogus because they don't have any delta lines. Additionally, the numbers in the @@ line become nonsense when more than one bogus hunk is filtered. Fix both of these issues affecting the bogus hunk filter logic. --- src/interdiff.c | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 0a46ae39..1ebf6a2d 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1525,8 +1525,9 @@ split_patch_hunks (FILE *patch, size_t len, char *file, do { /* nctx[0] = pre-context lines, nctx[1] = post-context lines * ndelta[0] = deleted lines, ndelta[1] = added lines */ - unsigned long nctx[2] = {}, ndelta[2] = {}, nctx_target; + unsigned long nctx[2] = {}, ndelta[2] = {}; unsigned long ostart, nstart, orig_nstart, start_line_idx = 0; + unsigned long nctx_target = 0; /* Init for spurious GCC warn */ struct xtra_context xctx_pre = {}; struct line_info *lines = NULL; unsigned long num_lines = 0; @@ -1587,14 +1588,15 @@ split_patch_hunks (FILE *patch, size_t len, char *file, lines[num_lines - 1].len = line - lines[num_lines - 1].s; - /* Check if this is the end. If so, terminate the hunk - * now because there isn't any new line to parse. */ + /* Count the number of characters left to parse */ hlen_rem = hunk + hlen - line; - if (!hlen_rem) - goto split_hunk_incl_latest; - /* Check if this is an unline that we need to remove */ - if (unline && !strncmp (line + 1, unline, unline_len)) { + /* Check if this is an unline that we need to remove, or + * if this is a bogus hunk. A bogus hunk may not have an + * unline as its final line, hence we need to consider + * this when there are no more lines left to parse. */ + if (unline && (!hlen_rem || !strncmp (line + 1, unline, + unline_len))) { /* Split the hunk now if there's a delta, unless * this is a bogus hunk from a rejected patch * hunk. Bogus hunks stem from one side of the @@ -1627,8 +1629,16 @@ split_patch_hunks (FILE *patch, size_t len, char *file, skipped_lines = 1; continue; } + + /* Bogus hunk, reset the delta counts */ + ndelta[0] = ndelta[1] = 0; } + /* Stop now when nothing remains, since all that + * we've got here is a bogus hunk to discard. */ + if (!hlen_rem) + break; + /* Move forward the starting line offset, * discarding any pre-context lines seen. The * starting line index is set to the _next_ @@ -1639,6 +1649,11 @@ split_patch_hunks (FILE *patch, size_t len, char *file, continue; } + /* Check if this is the end. If so, terminate the hunk + * now because there isn't any new line to parse. */ + if (!hlen_rem) + goto split_hunk_incl_latest; + /* Record the current line, setting `len` to zero */ lines = xrealloc (lines, ++num_lines * sizeof (*lines)); lines[num_lines - 1] = (typeof (*lines)){ line }; From 9059013b8046d80b99576f4fcdacb8fdcab7b2c2 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 23 Jan 2026 16:27:34 -0800 Subject: [PATCH 08/28] interdiff: Use -N on patch all the time So that patch never tries to prompt and ask for something when running interdiff on an interactive terminal. --- src/interdiff.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 1ebf6a2d..3cd9abba 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -973,7 +973,7 @@ output_patch1_only (FILE *p1, FILE *out, int not_reverted) static int apply_patch (FILE *patch, const char *file, int reverted, FILE **out) { -#define MAX_PATCH_ARGS 9 +#define MAX_PATCH_ARGS 8 const char *argv[MAX_PATCH_ARGS]; int argc = 0; const char *basename; @@ -1002,8 +1002,8 @@ apply_patch (FILE *patch, const char *file, int reverted, FILE **out) /* Add up to MAX_PATCH_ARGS arguments for the patch execution */ argv[argc++] = PATCH; - argv[argc++] = reverted ? (has_ignore_all_space ? "-Rlp0" : "-Rp0") - : (has_ignore_all_space ? "-lp0" : "-p0"); + argv[argc++] = reverted ? (has_ignore_all_space ? "-NRlp0" : "-NRp0") + : (has_ignore_all_space ? "-Nlp0" : "-Np0"); if (fuzzy) { int fuzz = 0; @@ -1011,14 +1011,12 @@ apply_patch (FILE *patch, const char *file, int reverted, FILE **out) argv[argc++] = "--no-backup-if-mismatch"; /* When reverting a rejected hunk, use the maximum possible - * fuzz, don't generate .rej files, and don't let patch ask to - * unreverse our hunk. Otherwise, either pass in the user- - * supplied max fuzz, or fuzz all but one pre-context and one - * post-context line by default. */ + * fuzz and don't generate .rej files. Otherwise, either pass in + * the user-supplied max fuzz, or fuzz all but one pre-context + * and one post-context line by default. */ if (reverted) { fuzz = INT_MAX; argv[argc++] = "--reject-file=-"; - argv[argc++] = "-N"; } else if (max_fuzz_user >= 0) { fuzz = max_fuzz_user; } else if (max_context) { From 4f507d00260435dc101fb6966ce2b673d544da61 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 22 Dec 2025 17:43:05 -0800 Subject: [PATCH 09/28] interdiff: Begin fixing the direction of delta differences in fuzzy mode Fuzzy mode output is incoherent because context differences show up as going from patch1 -> patch2, while delta differences show up in the opposite direction: patch2 -> patch1. This makes it rather impossible to tell which patch file a +/- line originates from, not to mention that the diff itself is totally nonsense in terms of the actual code changed by the patches. Bring interdiff part of the way there to fixing this issue, by eliminating delta differences from the compared files. Note that this makes fuzzy mode only do context diffing, which is fixed in the subsequent commits. --- src/interdiff.c | 138 +++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 3cd9abba..96792570 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -970,8 +970,39 @@ output_patch1_only (FILE *p1, FILE *out, int not_reverted) return 0; } +static void +open_rej_file (char *file, struct rej_file *rej) +{ + char *line = NULL; + size_t linelen; + long atat_pos; + + /* Briefly modify `file` in-place to open the .rej file */ + strcat (file, ".rej"); + rej->fp = xopen (file, "r"); + file[strlen (file) - strlen (".rej")] = '\0'; + + /* Skip (the first two) lines to get to the start of the @@ line */ + do { + atat_pos = ftell (rej->fp); + if (getline (&line, &linelen, rej->fp) <= 0) + error (EXIT_FAILURE, errno, + "Failed to read line from .rej"); + } while (strncmp (line, "@@ ", 3)); + fseek (rej->fp, atat_pos, SEEK_SET); + + /* Export the line offset of the first rej hunk */ + if (read_atatline (line, &rej->off, NULL, NULL, NULL)) + error (EXIT_FAILURE, 0, "line not understood: %s", line); + free (line); + + /* Go back to the @@ after apply_patch() moved the file cursor */ + fseek (rej->fp, atat_pos, SEEK_SET); +} + static int -apply_patch (FILE *patch, const char *file, int reverted, FILE **out) +apply_patch (FILE *patch, const char *file, int reverted, struct rej_file *rej, + FILE **out) { #define MAX_PATCH_ARGS 8 const char *argv[MAX_PATCH_ARGS]; @@ -1010,18 +1041,12 @@ apply_patch (FILE *patch, const char *file, int reverted, FILE **out) /* Don't generate .orig files when we expect rejected hunks */ argv[argc++] = "--no-backup-if-mismatch"; - /* When reverting a rejected hunk, use the maximum possible - * fuzz and don't generate .rej files. Otherwise, either pass in - * the user-supplied max fuzz, or fuzz all but one pre-context - * and one post-context line by default. */ - if (reverted) { - fuzz = INT_MAX; - argv[argc++] = "--reject-file=-"; - } else if (max_fuzz_user >= 0) { + /* Either pass in the user-supplied max fuzz, or fuzz all but + * one pre-context and one post-context line by default. */ + if (max_fuzz_user >= 0) fuzz = max_fuzz_user; - } else if (max_context) { + else if (max_context) fuzz = max_context - 1; - } if (asprintf (&fuzz_arg, "--fuzz=%d", fuzz) < 0) error (EXIT_FAILURE, errno, "asprintf failed"); argv[argc++] = fuzz_arg; @@ -1096,7 +1121,9 @@ apply_patch (FILE *patch, const char *file, int reverted, FILE **out) new_lines--; } fclose (w); + free (line); waitpid (child, &status, 0); + status = WEXITSTATUS (status); /* Provide the output from patch if requested */ if (out) @@ -1104,10 +1131,11 @@ apply_patch (FILE *patch, const char *file, int reverted, FILE **out) else fclose (r); - if (line) - free (line); + /* Open the reject file if requested and there are rejects */ + if (status && rej) + open_rej_file ((char *) file, rej); - return WEXITSTATUS (status); + return status; } static int @@ -2069,42 +2097,6 @@ fuzzy_relocate_hunks (const char *file, const char *unline, FILE *patch_out, free (relocs); } -static void -fuzzy_do_rej (char *file, struct rej_file *rej, const char *other_file) -{ - char *line = NULL; - size_t linelen; - long atat_pos; - - /* Briefly modify `file` in-place to open the .rej file */ - strcat (file, ".rej"); - rej->fp = xopen (file, "r"); - file[strlen (file) - strlen (".rej")] = '\0'; - - /* Skip (the first two) lines to get to the start of the @@ line */ - do { - atat_pos = ftell (rej->fp); - if (getline (&line, &linelen, rej->fp) <= 0) - error (EXIT_FAILURE, errno, - "Failed to read line from .rej"); - } while (strncmp (line, "@@ ", 3)); - fseek (rej->fp, atat_pos, SEEK_SET); - - /* Export the line offset of the first rej hunk */ - if (read_atatline (line, &rej->off, NULL, NULL, NULL)) - error (EXIT_FAILURE, 0, "line not understood: %s", line); - free (line); - - /* Revert the rejected hunks on the _other_ file, so they're excluded - * from the 'diff' output. Otherwise, 'diff' will output the _reverse_ - * of the rejected hunks, which will muddy the final output as we will - * print out the rejected hunks themselves later anyway. */ - apply_patch (rej->fp, other_file, 1, NULL); - - /* Go back to the @@ after apply_patch() moved the file cursor */ - fseek (rej->fp, atat_pos, SEEK_SET); -} - static void fuzzy_cleanup (char *file, int rej) { @@ -2219,25 +2211,18 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fseek (p1, start1, SEEK_SET); fseek (p2, start2, SEEK_SET); - /* Write it out. */ if (fuzzy) { + FILE *patch_out, *sp; + unsigned long *hunk_offs = NULL; + /* Ensure the same unline is used for both files */ write_file (&file, tmpp2fd); file2.unline = xstrdup (file.unline); write_file (&file2, tmpp1fd); - } else { - merge_lines (&file, &file2); - write_file (&file, tmpp1fd); - write_file (&file, tmpp2fd); - } - - if (fuzzy) { - unsigned long *hunk_offs = NULL; - FILE *patch_out, *sp; /* Split the patch hunks into smaller hunks, then apply that */ sp = split_patch_hunks (p2, pos2 - start2, tmpp1, &hunk_offs, NULL); - ret1 = apply_patch (sp, tmpp1, false, &patch_out); + ret1 = apply_patch (sp, tmpp1, false, &rej1, &patch_out); fclose (sp); /* Relocate hunks in tmpp1 in order to make them align with the @@ -2246,9 +2231,14 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fclose (patch_out); free (hunk_offs); + /* Revert the successful p2 deltas from tmpp1 so they don't + * appear as minus lines in the final diff. */ + fseek (p2, start2, SEEK_SET); + apply_patch (p2, tmpp1, true, NULL, NULL); + /* Split the patch hunks into smaller hunks, then apply that */ sp = split_patch_hunks (p1, pos1 - start1, tmpp2, NULL, NULL); - ret2 = apply_patch (sp, tmpp2, false, NULL); + ret2 = apply_patch (sp, tmpp2, false, &rej2, NULL); fclose (sp); /* For tmpp2 relocations, only eat unline gaps between hunks @@ -2256,19 +2246,21 @@ output_delta (FILE *p1, FILE *p2, FILE *out) * also done to tmpp1 during its relocation pass. */ fuzzy_relocate_hunks (tmpp2, file.unline, NULL, NULL); - /* Handle the rejected hunks. This needs to be done after both - * files are patched because it may revert a rejected hunk from - * the other file. */ - if (ret1) - fuzzy_do_rej (tmpp1, &rej1, tmpp2); - if (ret2) - fuzzy_do_rej (tmpp2, &rej2, tmpp1); + /* Revert the successful p1 deltas from tmpp2 so they don't + * appear as plus lines in the final diff. */ + fseek (p1, start1, SEEK_SET); + apply_patch (p1, tmpp2, true, NULL, NULL); } else { - if (apply_patch (p1, tmpp1, mode == mode_combine, NULL)) + /* Write it out. */ + merge_lines (&file, &file2); + write_file (&file, tmpp1fd); + write_file (&file, tmpp2fd); + + if (apply_patch (p1, tmpp1, mode == mode_combine, NULL, NULL)) error (EXIT_FAILURE, 0, "Error applying patch1 to reconstructed file"); - if (apply_patch (p2, tmpp2, 0, NULL)) + if (apply_patch (p2, tmpp2, 0, NULL, NULL)) error (EXIT_FAILURE, 0, "Error applying patch2 to reconstructed file"); } @@ -2858,7 +2850,7 @@ flipdiff (FILE *p1, FILE *p2, FILE *flip1, FILE *flip2) tmpfd = xmkstemp (tmpp1); write_file (&intermediate, tmpfd); fsetpos (p1, &at1); - if (apply_patch (p1, tmpp1, 1, NULL)) + if (apply_patch (p1, tmpp1, 1, NULL, NULL)) error (EXIT_FAILURE, 0, "Error reconstructing original file"); @@ -2867,7 +2859,7 @@ flipdiff (FILE *p1, FILE *p2, FILE *flip1, FILE *flip2) tmpfd = xmkstemp (tmpp3); write_file (&intermediate, tmpfd); fsetpos (p2, &at2); - if (apply_patch (p2, tmpp3, 0, NULL)) + if (apply_patch (p2, tmpp3, 0, NULL, NULL)) error (EXIT_FAILURE, 0, "Error reconstructing final file"); From 89544e4d18ff05e9a5cf301dd04c398228b45391 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 15 Jan 2026 17:29:54 -0800 Subject: [PATCH 10/28] interdiff: Fix hunk lookup to use actual applied position, not adjusted one When using --fuzzy mode, interdiff splits patch hunks and applies them with fuzz to maximize successful application. The function parse_fuzzed_hunks() parses patch's output to create relocation records that track where hunks were applied and how much they need to be relocated back to their original intended positions. The bug was in how the relocation's `new` field was calculated. The code was storing: new = lnum - hunk_offs[hnum - 1] where `lnum` is the line number where patch applied the hunk, and `hunk_offs` is the offset introduced by splitting the original hunk into smaller pieces. This calculation gives the "original hunk's intended line number" before any splitting occurred. However, fuzzy_relocate_hunks() later searches for hunks in the patched file by looking for hunks where `hcurr->nstart == rcurr->new`. The hunks in the patched file are located at their *actual* applied positions (i.e., `lnum`), not at the adjusted position (`lnum - hunk_offs`). This mismatch caused the relocation logic to fail to find the target hunk, triggering the fatal "failed to relocate hunk" error. The fix changes the relocation record to store: new = lnum (actual position in patched file) off = off + split_off (total offset: patch + split offset) This ensures that: 1. Hunks can be found at their actual positions in the patched file (matching `hcurr->nstart == rcurr->new` succeeds) 2. When relocating, subtracting `off` correctly moves the hunk back to its original intended position, accounting for both the patch offset (where patch chose to apply the hunk) and the split offset (the difference introduced by hunk splitting) 3. Duplicate detection still works correctly by comparing original intended positions: `lnum - off - split_off == prev->new - prev->off` simplifies to comparing the same values as before since prev->off now also includes the split offset Co-authored-by: Claude Opus 4.5 --- src/interdiff.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 96792570..5c6209bb 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1853,7 +1853,7 @@ parse_fuzzed_hunks (FILE *patch_out, const unsigned long *hunk_offs, /* Parse out each fuzzed hunk's line offset */ while (getline (&line, &linelen, patch_out) > 0) { struct hunk_reloc *prev = &(*relocs)[*num_relocs - 1]; - unsigned long fuzz = 0, hnum, lnum; + unsigned long fuzz = 0, hnum, lnum, split_off; long off; if (sscanf (line, "Hunk #%lu succeeded at %lu (offset %ld", @@ -1862,16 +1862,22 @@ parse_fuzzed_hunks (FILE *patch_out, const unsigned long *hunk_offs, &hnum, &lnum, &fuzz, &off) != 4) continue; - /* Recover the correct new line number of the possibly-split - * hunk, and skip it if it matches the relocated new line number - * of the previous hunk (if any). Split hunks are contiguous. */ - lnum -= hunk_offs[hnum - 1]; - if (*relocs && lnum - off == prev->new - prev->off) + /* Get the split offset for this hunk - the difference between + * the split hunk's new line number and the original hunk's. */ + split_off = hunk_offs[hnum - 1]; + + /* Skip if this hunk belongs to the same original hunk as the + * previous relocation. Split hunks are contiguous. Compare + * original line numbers (applied position - total offset). */ + if (*relocs && lnum - off - split_off == prev->new - prev->off) continue; + /* Store the actual applied position in the patched file as `new`, + * and the total offset (patch offset + split offset) needed to + * relocate the hunk back to its original intended position. */ *relocs = xrealloc (*relocs, ++*num_relocs * sizeof (**relocs)); (*relocs)[*num_relocs - 1] = - (typeof (**relocs)){ lnum, off, fuzz }; + (typeof (**relocs)){ lnum, off + split_off, fuzz }; } free (line); } From 0a30eff504d62eb497c06b59e292649519304820 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 23 Jan 2026 17:03:55 -0800 Subject: [PATCH 11/28] interdiff: Stop modifying file strings in-place This is a slippery slope and tricky to keep track of to ensure that the file string buffer is sufficiently large. Just use dynamic allocations. --- src/interdiff.c | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 5c6209bb..1970396a 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -971,16 +971,17 @@ output_patch1_only (FILE *p1, FILE *out, int not_reverted) } static void -open_rej_file (char *file, struct rej_file *rej) +open_rej_file (const char *file, struct rej_file *rej) { - char *line = NULL; + char *rej_file, *line = NULL; size_t linelen; long atat_pos; - /* Briefly modify `file` in-place to open the .rej file */ - strcat (file, ".rej"); - rej->fp = xopen (file, "r"); - file[strlen (file) - strlen (".rej")] = '\0'; + /* Open the .rej file */ + if (asprintf (&rej_file, "%s.rej", file) < 0) + error (EXIT_FAILURE, errno, "asprintf failed"); + rej->fp = xopen (rej_file, "r"); + free (rej_file); /* Skip (the first two) lines to get to the start of the @@ line */ do { @@ -1133,7 +1134,7 @@ apply_patch (FILE *patch, const char *file, int reverted, struct rej_file *rej, /* Open the reject file if requested and there are rejects */ if (status && rej) - open_rej_file ((char *) file, rej); + open_rej_file (file, rej); return status; } @@ -1542,10 +1543,13 @@ split_patch_hunks (FILE *patch, size_t len, char *file, /* Find the length of the unline now to use it in the loop */ unline_len = strlen (unline); } else { - /* Create the output file by temporarily modifying `file` */ - strcat (file, ".patch"); - out = xopen (file, "w+"); - file[strlen (file) - strlen (".patch")] = '\0'; + char *out_file; + + /* Create the output file */ + if (asprintf (&out_file, "%s.patch", file) < 0) + error (EXIT_FAILURE, errno, "asprintf failed"); + out = xopen (out_file, "w+"); + free (out_file); } do { @@ -2104,23 +2108,25 @@ fuzzy_relocate_hunks (const char *file, const char *unline, FILE *patch_out, } static void -fuzzy_cleanup (char *file, int rej) +fuzzy_cleanup (const char *file, int rej) { - /* Modify the `file` string in-place */ - char *end = strchr (file, '\0'); + size_t len = strlen (file); + char *tmp = xmalloc (len + sizeof (".patch")); + char *end = &tmp[len]; + + memcpy (tmp, file, len); /* Remove the .rej file if one was generated */ if (rej) { strcpy (end, ".rej"); - unlink (file); + unlink (tmp); } /* Remove the .patch file generated from splitting up the hunks */ strcpy (end, ".patch"); - unlink (file); + unlink (tmp); - /* Terminate `file` back at where it was terminated originally */ - *end = '\0'; + free (tmp); } static int @@ -2128,9 +2134,8 @@ output_delta (FILE *p1, FILE *p2, FILE *out) { const char *tmpdir = getenv ("TMPDIR"); unsigned int tmplen; - /* Reserve space for appending .rej and .patch at the end of tmpp1/2 */ - const char tail1[] = "/interdiff-1.XXXXXX\0patch"; - const char tail2[] = "/interdiff-2.XXXXXX\0patch"; + const char tail1[] = "/interdiff-1.XXXXXX"; + const char tail2[] = "/interdiff-2.XXXXXX"; char *tmpp1, *tmpp2; int tmpp1fd, tmpp2fd; struct lines_info file = { NULL, 0, 0, NULL, NULL }; From 1702c7924523306ef0bdcc169b65ec40360d355b Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 4 Feb 2026 14:50:05 -0800 Subject: [PATCH 12/28] interdiff: Add run_diff() helper function Extract the common diff invocation pattern into a reusable run_diff() helper function. This function: - Takes options string as a parameter (caller builds it) - Builds the diff command with the options and diff_opts - Executes diff via xpipe() - Consumes the --- and +++ header lines - Returns a FILE* positioned at the first @@ line, or NULL if empty - Optionally returns the child pid for the caller to waitpid() Refactor output_delta() to use run_diff() instead of inline diff code. This simplifies the function and prepares for additional diff call sites. Assisted-by: Claude Opus 4.5 --- src/interdiff.c | 78 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 1970396a..5556e6b0 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -2129,6 +2129,55 @@ fuzzy_cleanup (const char *file, int rej) free (tmp); } +/* Run diff on two files and return a FILE* positioned at the first @@ line. + * The --- and +++ header lines are consumed. + * + * Returns NULL if diff is empty. The caller must waitpid() on the returned pid + * when return is non-NULL. */ +static FILE * +run_diff (const char *options, const char *file1, const char *file2, + pid_t *child_out) +{ + pid_t child; + FILE *in; + int diff_is_empty = 1; + + fflush (NULL); + + char *argv[2 + num_diff_opts + 2 + 1]; + memcpy (argv, ((const char *[]) { DIFF, options }), 2 * sizeof (char *)); + memcpy (argv + 2, diff_opts, num_diff_opts * sizeof (char *)); + memcpy (argv + 2 + num_diff_opts, + ((char *[]) { (char *)file1, (char *)file2, NULL }), + (2 + 1) * sizeof (char *)); + in = xpipe (DIFF, &child, "r", argv); + + /* Eat the first line (--- ...) */ + for (;;) { + int ch = fgetc (in); + if (ch == EOF || ch == '\n') + break; + diff_is_empty = 0; + } + + /* Eat the second line (+++ ...) */ + for (;;) { + int ch = fgetc (in); + if (ch == EOF || ch == '\n') + break; + } + + *child_out = child; + + if (diff_is_empty) { + fclose (in); + waitpid (child, NULL, 0); + return NULL; + } + + return in; +} + static int output_delta (FILE *p1, FILE *p2, FILE *out) { @@ -2278,28 +2327,8 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fseek (p1, pos1, SEEK_SET); - fflush (NULL); - - char *argv[2 + num_diff_opts + 2 + 1]; - memcpy (argv, ((const char *[]) { DIFF, options }), 2 * sizeof (char *)); - memcpy (argv + 2, diff_opts, num_diff_opts * sizeof (char *)); - memcpy (argv + 2 + num_diff_opts, ((char *[]) { tmpp1, tmpp2, NULL }), (2 + 1) * sizeof (char *)); - in = xpipe (DIFF, &child, "r", argv); - - /* Eat the first line */ - for (;;) { - int ch = fgetc (in); - if (ch == EOF || ch == '\n') - break; - diff_is_empty = 0; - } - - /* Eat the second line */ - for (;;) { - int ch = fgetc (in); - if (ch == EOF || ch == '\n') - break; - } + in = run_diff (options, tmpp1, tmpp2, &child); + diff_is_empty = !in; /* Rebuild the diff hunks without unlines, since fuzzy diffing shows * context line differences that therefore may cause unlines to appear @@ -2379,9 +2408,10 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fclose (tmpdiff); } - if (in) + if (in) { fclose (in); - waitpid (child, NULL, 0); + waitpid (child, NULL, 0); + } if (debug) printf ("reconstructed orig1=%s orig2=%s\n", tmpp1, tmpp2); else { From 9fafcd937e933b333bfaa913888fdf8bfbd11240 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 16:45:46 -0800 Subject: [PATCH 13/28] interdiff: Add skip_header_lines() helper function Replace the inline getline/fseek header-skipping loop in the function fuzzy_output_list() and the fgetc loops in run_diff() with a shared skip_header_lines() helper that skips the two --- / +++ lines. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 5556e6b0..3c6cac40 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1300,6 +1300,18 @@ trim_context (FILE *f /* positioned at start of @@ line */, return 0; } +/* Skip past the two --- / +++ header lines. Returns 1 on EOF. */ +static int +skip_header_lines (FILE *f) +{ + int ch; + + for (int i = 0; i < 2; i++) + while ((ch = fgetc (f)) != '\n' && ch != EOF); + + return ch == EOF; +} + static void output_rej_hunks (const char *diff, struct rej_file **rej1, struct rej_file **rej2, FILE *out) @@ -2140,7 +2152,6 @@ run_diff (const char *options, const char *file1, const char *file2, { pid_t child; FILE *in; - int diff_is_empty = 1; fflush (NULL); @@ -2152,24 +2163,10 @@ run_diff (const char *options, const char *file1, const char *file2, (2 + 1) * sizeof (char *)); in = xpipe (DIFF, &child, "r", argv); - /* Eat the first line (--- ...) */ - for (;;) { - int ch = fgetc (in); - if (ch == EOF || ch == '\n') - break; - diff_is_empty = 0; - } - - /* Eat the second line (+++ ...) */ - for (;;) { - int ch = fgetc (in); - if (ch == EOF || ch == '\n') - break; - } - *child_out = child; - if (diff_is_empty) { + /* Skip past the --- / +++ lines output by diff */ + if (skip_header_lines (in)) { fclose (in); waitpid (child, NULL, 0); return NULL; From ac04fa3f6f8cf5559cf3a17e649d0ae9bf5be5a6 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 16:47:56 -0800 Subject: [PATCH 14/28] interdiff: Separate delta and context differences in fuzzy mode Refactor fuzzy mode to produce clearly separated output sections that distinguish between actual code changes (delta differences) and surrounding context changes (context differences). This makes it much easier for reviewers to understand backport patches by showing: 1. What behavioral changes differ between the patches 2. What contextual adaptations were made for the target codebase 3. Which hunks couldn't be applied and need manual review DELTA DIFFING: The goal is to show code changes present in one patch but not the other, even when patch2 has hunks that fail to apply due to context mismatch. 1. Construct patch1_orig and patch1_new from patch1 2. Split patch2 and apply to patch1_orig with normal fuzz 3. Generate delta_diff = diff(patch1_new, patch1_orig + patch2) 4. Filter delta hunks that are just the inverse of rejected patch2 hunks (see REJECTION FILTERING below) 5. Filter bogus hunks containing unlines via split_patch_hunks() CONTEXT DIFFING: The goal is to show only context differences with delta changes removed. 1. Create a fresh copy of patch1_orig (delta diffing modified tmpp1) 2. Apply delta_diff in reverse to patch2_orig to remove delta changes 3. Parse fuzz offsets and relocate hunks in patch2_orig 4. Generate ctx_diff = diff(patch1_orig, patch2_orig) 5. Filter bogus hunks containing unlines via split_patch_hunks() REJECTION FILTERING: When patch2 has a hunk that was rejected on patch1_orig, and patch1 makes the same change, the delta diff will show a bogus difference (patch1_new has the change but tmpp1 doesn't). Rather than reverse-applying rejected hunks with the patch utility (which is unreliable when multiple similar code patterns exist), delta_diff is post-processed to filter these out: 1. Parse all delta hunks and rejected hunks, extracting +/- lines and context lines with their distances from the nearest change line. 2. For each rejected hunk, find the best matching delta hunk: the rejected hunk's +lines must appear as a contiguous segment in the delta's -lines (and vice versa). Among matches, pick the delta hunk with the highest context score. The score is the sum of matching context line lengths divided by distance from the nearest change, so lines adjacent to changes are weighted most heavily (similar to how the patch utility prioritizes inner context for fuzzy matching). 3. Each rejected hunk is assigned to at most one delta hunk. A single delta hunk may have multiple rejected hunks assigned (when diff merged adjacent rejected changes into one hunk). 4. A delta hunk is filtered only if ALL its change lines are fully covered by assigned rejected hunks, verified by greedy sequential matching. OUTPUT STRUCTURE: Output is organized into distinct sections with 80-column ASCII banners. Delta differences use = borders with * sides, rejected hunks use # borders with ! sides, and context differences use = borders with * sides. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 769 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 615 insertions(+), 154 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 3c6cac40..faa581e3 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -62,6 +62,22 @@ #define PATCH "patch" #endif +/* Fuzzy mode section headers */ +#define DELTA_DIFF_HEADER \ + "================================================================================\n" \ + "* DELTA DIFFERENCES - code changes that differ between the patches *\n" \ + "================================================================================\n\n" + +#define DELTA_REJ_HEADER \ + "################################################################################\n" \ + "! REJECTED PATCH2 HUNKS - could not be compared; manual review needed !\n" \ + "################################################################################\n\n" + +#define CONTEXT_DIFF_HEADER \ + "================================================================================\n" \ + "* CONTEXT DIFFERENCES - surrounding code differences between the patches *\n" \ + "================================================================================\n\n" + /* Line type for coloring */ enum line_type { LINE_FILE, @@ -160,6 +176,35 @@ static int debug = 0; static int fuzzy = 0; static int max_fuzz_user = -1; +/* Per-file record for fuzzy mode output accumulation */ +struct fuzzy_file_record { + char *oldname; + char *newname; + FILE *hunks; /* Raw hunk data (before trim_context) */ + struct fuzzy_file_record *next; +}; + +/* Per-hunk change/context line arrays for rejection filtering */ +struct strarray { + struct line_info *lines; + size_t len; +}; + +struct hunk_lines { + struct strarray add, del, ctx; + unsigned int *ctx_dist; /* Distance from nearest +/- line */ +}; + +struct fuzzy_file_list { + struct fuzzy_file_record *head; + struct fuzzy_file_record *tail; +}; + +/* Accumulators for fuzzy mode output sections */ +static struct fuzzy_file_list fuzzy_delta_files = {}; +static struct fuzzy_file_list fuzzy_ctx_files = {}; +static struct fuzzy_file_list fuzzy_delta_rej_files = {}; + static struct patlist *pat_drop_context = NULL; static struct file_list *files_done = NULL; @@ -1158,12 +1203,9 @@ trim_context (FILE *f /* positioned at start of @@ line */, unsigned long orig_count, orig_orig_count, new_orig_count; unsigned long new_count, orig_new_count, new_new_count; unsigned long total_count = 0; - char *atat_comment; - ssize_t got; /* Read @@ line. */ - got = getline (&line, &linelen, f); - if (got < 0) + if (getline (&line, &linelen, f) < 0) break; if (line[0] == '\\') { @@ -1173,16 +1215,10 @@ trim_context (FILE *f /* positioned at start of @@ line */, } if (read_atatline (line, &orig_offset, &orig_count, - &new_offset, &new_count) || - !(atat_comment = strstr (line + 1, "@@"))) + &new_offset, &new_count)) error (EXIT_FAILURE, 0, "Line not understood: %s", line); - /* Check if there's a comment after the @@ line to retain */ - if (atat_comment + 3 - line < got) - atat_comment = xstrdup (atat_comment + 2); - else - atat_comment = NULL; orig_orig_count = new_orig_count = orig_count; orig_new_count = new_new_count = new_count; fgetpos (f, &pos); @@ -1199,12 +1235,12 @@ trim_context (FILE *f /* positioned at start of @@ line */, if (new_count) new_count--; if (!pre_seen) { pre++; - if (!strcmp (line + 1, unline)) + if (unline && !strcmp (line + 1, unline)) strip_pre = pre; } else { post++; if (strip_post || - !strcmp (line + 1, unline)) + (unline && !strcmp (line + 1, unline))) strip_post++; } break; @@ -1243,25 +1279,18 @@ trim_context (FILE *f /* positioned at start of @@ line */, fsetpos (f, &pos); if (new_orig_count != 1 && new_new_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu,%lu @@", + print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu,%lu @@\n", orig_offset, new_orig_count, new_offset, new_new_count); else if (new_orig_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu @@", + print_color (out, LINE_HUNK, "@@ -%lu,%lu +%lu @@\n", orig_offset, new_orig_count, new_offset); else if (new_new_count != 1) - print_color (out, LINE_HUNK, "@@ -%lu +%lu,%lu @@", + print_color (out, LINE_HUNK, "@@ -%lu +%lu,%lu @@\n", orig_offset, new_offset, new_new_count); else - print_color (out, LINE_HUNK, "@@ -%lu +%lu @@", + print_color (out, LINE_HUNK, "@@ -%lu +%lu @@\n", orig_offset, new_offset); - if (atat_comment) { - fputs (atat_comment, out); - free (atat_comment); - } else { - fputc ('\n', out); - } - while (total_count--) { enum line_type type; ssize_t got = getline (&line, &linelen, f); @@ -1312,83 +1341,65 @@ skip_header_lines (FILE *f) return ch == EOF; } +/* Add a file record to a fuzzy output list */ static void -output_rej_hunks (const char *diff, struct rej_file **rej1, - struct rej_file **rej2, FILE *out) +fuzzy_add_file (struct fuzzy_file_list *list, const char *oldname, + const char *newname, FILE *hunks) { - char *line = NULL; + struct fuzzy_file_record *rec = xmalloc (sizeof (*rec)); - while (*rej1 || *rej2) { - struct rej_file **rej_ptr = rej1, *rej; - int first_line_done = 0, patch_id = 1; - unsigned long diff_off; - long next_atat_pos; - size_t linelen; - ssize_t got; + rec->oldname = xstrdup (oldname); + rec->newname = xstrdup (newname); + rec->hunks = hunks; + rec->next = NULL; - /* Pick the reject hunk that comes first */ - if (!*rej1 || (*rej2 && (*rej2)->off < (*rej1)->off)) { - rej_ptr = rej2; - patch_id = 2; - } - rej = *rej_ptr; + if (list->tail) + list->tail->next = rec; + else + list->head = rec; + list->tail = rec; +} - if (diff) { - /* Wait until the current diff line is an @@ line */ - if (strncmp (diff, "@@ ", 3)) - return; +/* Free a fuzzy file record list */ +static void +fuzzy_free_list (struct fuzzy_file_list *list) +{ + struct fuzzy_file_record *rec = list->head; - if (read_atatline (diff, &diff_off, NULL, NULL, NULL)) - error (EXIT_FAILURE, 0, "line not understood: %s", - diff); + while (rec) { + struct fuzzy_file_record *next = rec->next; - /* Stop if the diff hunk comes next */ - if (rej->off > diff_off) - return; - } + free (rec->oldname); + free (rec->newname); + if (rec->hunks) + fclose (rec->hunks); + free (rec); + rec = next; + } +} - /* Write the rej hunk until EOF or the next @@ line (i.e., next - * hunk). Note that rej starts at the current @@ line that we - * must write, so don't look for the next @@ until after the - * first line is written. */ - for (;;) { - got = getline (&line, &linelen, rej->fp); - if (got <= 0) { - if (feof (rej->fp)) - goto rej_file_eof; - error (EXIT_FAILURE, errno, - "Failed to read line from .rej"); - } - if (first_line_done) { - if (!strncmp (line, "@@ ", 3)) - break; +/* Output a fuzzy file list with colorization through trim_context. If + * skip_headers is set, skip past the --- / +++ lines (e.g. for reject files + * which include their own headers). */ +static void +fuzzy_output_list (struct fuzzy_file_list *list, int skip_headers, FILE *out) +{ + struct fuzzy_file_record *rec; - fwrite (line, (size_t) got, 1, out); - next_atat_pos = ftell (rej->fp); - } else { - /* Append a comment after the @@ line indicating - * this is a rejected hunk. */ - first_line_done = 1; - fwrite (line, (size_t) got - 1, 1, out); - fprintf (out, " INTERDIFF: rejected hunk from patch%d, cannot diff context\n", - patch_id); - } - } + for (rec = list->head; rec; rec = rec->next) { + if (!rec->hunks) + continue; - /* Record the line offset of the next rej hunk, if any */ - if (read_atatline (line, &rej->off, NULL, NULL, NULL)) - error (EXIT_FAILURE, 0, "line not understood: %s", line); - fseek (rej->fp, next_atat_pos, SEEK_SET); + rewind (rec->hunks); - if (!feof (rej->fp)) - continue; + /* Skip past --- / +++ headers */ + if (skip_headers && skip_header_lines (rec->hunks)) + error (EXIT_FAILURE, 0, "truncated hunk file"); -rej_file_eof: - /* Clear out this reject file pointer when it's finished */ - *rej_ptr = NULL; + print_color (out, LINE_FILE, "--- %s\n", rec->oldname); + print_color (out, LINE_FILE, "+++ %s\n", rec->newname); + trim_context (rec->hunks, NULL, out); } - - free (line); } /* `xctx` must come with `num` initialized and `s` and `len` zeroed */ @@ -1535,7 +1546,6 @@ split_patch_hunks (FILE *patch, size_t len, char *file, fbuf = xrealloc (fbuf, ++len + 1); fbuf[len - 1] = ch; } - fclose (patch); } fbuf[len] = '\0'; @@ -2175,6 +2185,361 @@ run_diff (const char *options, const char *file1, const char *file2, return in; } +static int +line_info_eq (const struct line_info *a, const struct line_info *b) +{ + return a->len == b->len && !memcmp (a->s, b->s, a->len); +} + +static void +strarray_push (struct strarray *sa, const char *str, size_t len) +{ + sa->lines = xrealloc (sa->lines, (sa->len + 1) * sizeof (*sa->lines)); + sa->lines[sa->len].s = xmalloc (len); + memcpy (sa->lines[sa->len].s, str, len); + sa->lines[sa->len].len = len; + sa->len++; +} + +static void +strarray_free (struct strarray *sa) +{ + for (size_t i = 0; i < sa->len; i++) + free (sa->lines[i].s); + free (sa->lines); +} + +/* Parse hunks from a FILE* into an array of hunk_lines. For delta hunks, also + * captures raw hunk content into raw_hunks[] for later output. + * + * Pass NULL for raw_hunks if not needed. */ +static size_t +parse_hunks (FILE *f, struct hunk_lines **out, struct line_info **raw_hunks) +{ + struct hunk_lines *hunks = NULL; + size_t nhunks = 0, linelen; + char *line = NULL; + ssize_t got; + + if (raw_hunks) + *raw_hunks = NULL; + + rewind (f); + while ((got = getline (&line, &linelen, f)) > 0) { + unsigned char *is_delta = NULL; + size_t nlines = 0, ctx_idx; + struct hunk_lines *h; + unsigned int dist; + + if (strncmp (line, "@@ ", 3)) + continue; + + nhunks++; + hunks = xrealloc (hunks, nhunks * sizeof (*hunks)); + h = &hunks[nhunks - 1]; + *h = (typeof (*h)){}; + + /* Save the @@ line as the start of the raw hunk content */ + if (raw_hunks) { + *raw_hunks = xrealloc (*raw_hunks, + nhunks * sizeof (**raw_hunks)); + (*raw_hunks)[nhunks - 1].s = xmalloc (got); + memcpy ((*raw_hunks)[nhunks - 1].s, line, got); + (*raw_hunks)[nhunks - 1].len = got; + } + + /* Read body lines until the next @@ or EOF, classifying each + * line as context or delta (+/-) and recording whether each + * line is a delta for distance computation. */ + while ((got = getline (&line, &linelen, f)) > 0) { + struct strarray *sa; + + if (!strncmp (line, "@@ ", 3)) { + fseek (f, -got, SEEK_CUR); + break; + } + + /* Append the raw line to the current hunk's buffer */ + if (raw_hunks) { + struct line_info *r = &(*raw_hunks)[nhunks - 1]; + + r->s = xrealloc (r->s, r->len + got); + memcpy (r->s + r->len, line, got); + r->len += got; + } + + is_delta = xrealloc (is_delta, nlines + 1); + if (line[0] == ' ') { + is_delta[nlines] = 0; + sa = &h->ctx; + } else { + is_delta[nlines] = 1; + sa = line[0] == '+' ? &h->add : &h->del; + } + strarray_push (sa, line + 1, got - 2); + nlines++; + } + + /* Forward pass: compute the distance of each context line from + * the nearest preceding delta line. Lines closest to deltas are + * most valuable for disambiguation. */ + h->ctx_dist = xmalloc (h->ctx.len * sizeof (*h->ctx_dist)); + dist = UINT_MAX; + ctx_idx = 0; + for (size_t k = 0; k < nlines; k++) { + if (is_delta[k]) { + dist = 0; + } else { + if (dist < UINT_MAX) + dist++; + h->ctx_dist[ctx_idx++] = dist; + } + } + + /* Backward pass: take the min of forward and backward distances + * so each context line reflects its distance from the nearest + * delta in either direction. */ + dist = UINT_MAX; + ctx_idx = h->ctx.len; + for (size_t k = nlines; k > 0; k--) { + if (is_delta[k - 1]) { + dist = 0; + } else { + ctx_idx--; + if (dist < UINT_MAX) + dist++; + if (dist < h->ctx_dist[ctx_idx]) + h->ctx_dist[ctx_idx] = dist; + } + } + + free (is_delta); + } + + free (line); + *out = hunks; + return nhunks; +} + +static void +free_hunk_lines (struct hunk_lines *hunks, size_t nhunks) +{ + for (size_t i = 0; i < nhunks; i++) { + strarray_free (&hunks[i].add); + strarray_free (&hunks[i].del); + strarray_free (&hunks[i].ctx); + free (hunks[i].ctx_dist); + } + free (hunks); +} + +/* Score how well the context lines from a rejected hunk match a delta hunk. + * Context lines closer to +/- changes are weighted more heavily, mirroring how + * the patch utility prioritizes inner context for fuzzy matching. The score is + * line_length / distance_from_nearest_change for each matching line. */ +static int +context_score (const struct hunk_lines *rej, const struct hunk_lines *delta) +{ + int score = 0; + + for (size_t i = 0; i < rej->ctx.len; i++) { + for (size_t j = 0; j < delta->ctx.len; j++) { + if (line_info_eq (&rej->ctx.lines[i], + &delta->ctx.lines[j])) { + unsigned int dist = rej->ctx_dist[i]; + + if (dist < delta->ctx_dist[j]) + dist = delta->ctx_dist[j]; + score += rej->ctx.lines[i].len / + (dist ? dist : 1); + break; + } + } + } + + return score; +} + +/* Check if a rejected hunk matches a delta hunk at the given positions. Since + * the delta is the inverse of the rejection, the rejected hunk's "+" lines are + * compared against the delta's "-" lines and vice versa. On match, *pos_del and + * *pos_add are advanced past the matched lines. */ +static int +rej_matches_delta_at (const struct hunk_lines *rej, + const struct hunk_lines *delta, + size_t *pos_del, size_t *pos_add) +{ + if (!rej->add.len && !rej->del.len) + return 0; + + if (rej->add.len) { + if (*pos_del + rej->add.len > delta->del.len) + return 0; + + for (size_t i = 0; i < rej->add.len; i++) { + if (!line_info_eq (&delta->del.lines[*pos_del + i], + &rej->add.lines[i])) + return 0; + } + } + + if (rej->del.len) { + if (*pos_add + rej->del.len > delta->add.len) + return 0; + + for (size_t i = 0; i < rej->del.len; i++) { + if (!line_info_eq (&delta->add.lines[*pos_add + i], + &rej->del.lines[i])) + return 0; + } + } + + *pos_del += rej->add.len; + *pos_add += rej->del.len; + return 1; +} + +/* Check if a rejected hunk's change lines appear anywhere in a delta hunk's + * change lines. Unlike rej_matches_delta_at(), this searches all positions. */ +static int +rej_matches_delta (const struct hunk_lines *rej, const struct hunk_lines *delta) +{ + if (rej->add.len > delta->del.len || rej->del.len > delta->add.len) + return 0; + + for (size_t pd = 0; pd <= delta->del.len - rej->add.len; pd++) { + for (size_t pa = 0; pa <= delta->add.len - rej->del.len; pa++) { + size_t tmp_pd = pd, tmp_pa = pa; + + if (rej_matches_delta_at (rej, delta, &tmp_pd, &tmp_pa)) + return 1; + } + } + + return 0; +} + +/* Filter delta diff hunks that are just the inverse of rejected hunks. When + * patch2 has a hunk that was rejected on patch1_orig, and patch1 makes the same + * change, the delta diff will show a bogus difference. This function removes + * those bogus hunks by comparing each delta hunk's change lines against the + * rejected hunks' change lines in reverse. + * + * Each rejected hunk is used at most once. When a rejected hunk matches + * multiple delta hunks, the one with the most matching context lines wins. + * A single delta hunk may span multiple rejected hunks that diff merged. + * + * Returns a new FILE* with the filtered output, or NULL if all hunks were + * filtered out. */ +static FILE * +filter_inverted_rejects (FILE *delta, FILE *rej) +{ + struct hunk_lines *rej_hunks, *delta_hunks; + struct line_info *raw_hunks; + size_t nrej, ndelta; + long *rej_assigned; + FILE *out = NULL; + + if (!(nrej = parse_hunks (rej, &rej_hunks, NULL)) || + !(ndelta = parse_hunks (delta, &delta_hunks, &raw_hunks))) + error (EXIT_FAILURE, 0, + "filter_inverted_rejects: no hunks parsed"); + + rej_assigned = xmalloc (nrej * sizeof (*rej_assigned)); + + /* For each rejected hunk, find the delta hunk whose change lines match + * (inverted) and which has the best context overlap. */ + for (size_t r = 0; r < nrej; r++) { + int best_score = -1; + + rej_assigned[r] = -1; + for (size_t d = 0; d < ndelta; d++) { + int score; + + if (!rej_matches_delta (&rej_hunks[r], &delta_hunks[d])) + continue; + + score = context_score (&rej_hunks[r], &delta_hunks[d]); + if (score > best_score) { + best_score = score; + rej_assigned[r] = d; + } + } + } + + /* For each delta hunk, check if all of its change lines are fully + * covered by the rejected hunks assigned to it (in order). */ + for (size_t d = 0; d < ndelta; d++) { + size_t pos_del = 0, pos_add = 0; + + for (size_t r = 0; r < nrej; r++) { + if (rej_assigned[r] == d && + !rej_matches_delta_at (&rej_hunks[r], + &delta_hunks[d], + &pos_del, &pos_add)) + break; + } + + /* Emit the delta hunk if it shouldn't be filtered */ + if (pos_del != delta_hunks[d].del.len || + pos_add != delta_hunks[d].add.len) { + if (!out) + out = xtmpfile (); + fwrite (raw_hunks[d].s, raw_hunks[d].len, 1, out); + } + free (raw_hunks[d].s); + } + + free (rej_assigned); + free (raw_hunks); + free_hunk_lines (delta_hunks, ndelta); + free_hunk_lines (rej_hunks, nrej); + return out; +} + +/* Run diff and filter out bogus hunks containing unlines. + * + * Returns NULL if the resulting diff is empty. */ +static FILE * +run_and_clean_diff (const char *options, const char *file1, const char *file2, + const char *unline) +{ + pid_t child; + FILE *diff; + + diff = run_diff (options, file1, file2, &child); + if (diff) { + FILE *sp; + + sp = split_patch_hunks (diff, 0, NULL, NULL, unline); + fclose (diff); + diff = sp; + } + waitpid (child, NULL, 0); + + return diff; +} + +/* Write a lines_info struct to a new temp file derived from the given template + * path (replaces the last 6 chars with XXXXXX for mkstemp). + * + * Returns the allocated filename which must be freed by the caller. */ +static char * +write_to_tmpfile (const char *tmpl, struct lines_info *info) +{ + char *file = xstrdup (tmpl); + int fd; + + strcpy (file + strlen (file) - 6, "XXXXXX"); + fd = mkstemp (file); + if (fd < 0) + error (EXIT_FAILURE, errno, "mkstemp failed"); + + write_file (info, fd); + close (fd); + return file; +} + static int output_delta (FILE *p1, FILE *p2, FILE *out) { @@ -2183,11 +2548,12 @@ output_delta (FILE *p1, FILE *p2, FILE *out) const char tail1[] = "/interdiff-1.XXXXXX"; const char tail2[] = "/interdiff-2.XXXXXX"; char *tmpp1, *tmpp2; + char *unline = NULL; int tmpp1fd, tmpp2fd; struct lines_info file = { NULL, 0, 0, NULL, NULL }; struct lines_info file2 = { NULL, 0, 0, NULL, NULL }; - struct rej_file rej1, rej2; - int ret1 = 0, ret2 = 0; + struct rej_file rej; + int patch_ret = 0, ctx_ret = 0; char *oldname = NULL, *newname = NULL; pid_t child; FILE *in; @@ -2196,7 +2562,6 @@ output_delta (FILE *p1, FILE *p2, FILE *out) long pristine1, pristine2; long start1, start2; char options[100]; - int diff_is_empty = 1; pristine1 = ftell (p1); pristine2 = ftell (p2); @@ -2269,44 +2634,117 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fseek (p2, start2, SEEK_SET); if (fuzzy) { - FILE *patch_out, *sp; - unsigned long *hunk_offs = NULL; - - /* Ensure the same unline is used for both files */ + unsigned long *hunk_offs = NULL, *ctx_hunk_offs = NULL; + char *patch1_new_file, *ctx_patch1_orig_file; + struct lines_info patch1_new_info = {}; + FILE *sp, *delta_diff, *ctx_diff; + FILE *ctx_patch_out = NULL; + int delta_empty, ctx_empty; + + /* Ensure the same unline is used for both files. + * file = patch2_orig, file2 = patch1_orig */ write_file (&file, tmpp2fd); - file2.unline = xstrdup (file.unline); + unline = file.unline; + file2.unline = unline; write_file (&file2, tmpp1fd); - /* Split the patch hunks into smaller hunks, then apply that */ + /* + * DELTA DIFFING: + * 1. Construct patch1_new from patch1 (reverted=1 -> new side) + * 2. Split patch2 and apply to tmpp1 (patch1_orig) + * 3. delta_diff = diff(patch1_new, tmpp1) + * 4. Filter delta hunks that match rejected patch2 hunks + * + * CONTEXT DIFFING: + * 1. Make a fresh copy of patch1_orig (tmpp1 is modified above) + * 2. Apply delta_diff in reverse to tmpp2 (patch2_orig) to + * remove delta differences + * 3. Relocate hunks using fuzz offsets + * 4. ctx_diff = diff(patch1_orig, patch2_orig) + */ + + /* Create patch1_new (reverted=1 gives the new side of patch1) */ + fseek (p1, start1, SEEK_SET); + create_orig (p1, &patch1_new_info, 1, NULL); + patch1_new_info.unline = unline; + patch1_new_file = write_to_tmpfile (tmpp1, &patch1_new_info); + free_lines (patch1_new_info.head); + + /* Split patch2 and apply to tmpp1 (patch1_orig) */ sp = split_patch_hunks (p2, pos2 - start2, tmpp1, &hunk_offs, NULL); - ret1 = apply_patch (sp, tmpp1, false, &rej1, &patch_out); + patch_ret = apply_patch (sp, tmpp1, 0, &rej, NULL); fclose (sp); - - /* Relocate hunks in tmpp1 in order to make them align with the - * positions of the hunks in tmpp2. */ - fuzzy_relocate_hunks (tmpp1, file.unline, patch_out, hunk_offs); - fclose (patch_out); free (hunk_offs); - /* Revert the successful p2 deltas from tmpp1 so they don't - * appear as minus lines in the final diff. */ - fseek (p2, start2, SEEK_SET); - apply_patch (p2, tmpp1, true, NULL, NULL); + /* Delta diff: diff(patch1_new, patch1_orig + patch2) */ + delta_diff = run_and_clean_diff (options, patch1_new_file, + tmpp1, unline); + delta_empty = !delta_diff; + + /* Filter bogus delta hunks that are just the inverse of rejected + * hunks (both patches make the same change but patch2's was + * rejected due to context mismatch) */ + if (patch_ret && rej.fp) { + /* rej.fp ownership transfers to the list; + * fuzzy_free_list() will fclose it. */ + fuzzy_add_file (&fuzzy_delta_rej_files, oldname + 4, + newname + 4, rej.fp); + + if (!delta_empty) { + FILE *filtered; + + rewind (rej.fp); + filtered = filter_inverted_rejects (delta_diff, + rej.fp); + fclose (delta_diff); + delta_diff = filtered; + delta_empty = !delta_diff; + } + } - /* Split the patch hunks into smaller hunks, then apply that */ - sp = split_patch_hunks (p1, pos1 - start1, tmpp2, NULL, NULL); - ret2 = apply_patch (sp, tmpp2, false, &rej2, NULL); - fclose (sp); + /* Apply delta_diff in reverse to tmpp2 (patch2_orig) to + * remove delta differences and isolate context diffs */ + if (!delta_empty) { + FILE *sp2; - /* For tmpp2 relocations, only eat unline gaps between hunks - * that amount to no more than max_context*2 lines. This was - * also done to tmpp1 during its relocation pass. */ - fuzzy_relocate_hunks (tmpp2, file.unline, NULL, NULL); + rewind (delta_diff); + sp2 = split_patch_hunks (delta_diff, 0, tmpp2, + &ctx_hunk_offs, NULL); + if (sp2) { + ctx_ret = apply_patch (sp2, tmpp2, 1, NULL, + &ctx_patch_out); + fclose (sp2); + } - /* Revert the successful p1 deltas from tmpp2 so they don't - * appear as plus lines in the final diff. */ - fseek (p1, start1, SEEK_SET); - apply_patch (p1, tmpp2, true, NULL, NULL); + if (ctx_patch_out && ctx_hunk_offs) + fuzzy_relocate_hunks (tmpp2, unline, + ctx_patch_out, + ctx_hunk_offs); + if (ctx_patch_out) + fclose (ctx_patch_out); + free (ctx_hunk_offs); + } + + /* Fresh copy of patch1_orig for context comparison + * since tmpp1 was modified by delta diffing above. */ + ctx_patch1_orig_file = write_to_tmpfile (tmpp1, &file2); + + ctx_diff = run_and_clean_diff (options, ctx_patch1_orig_file, + tmpp2, unline); + ctx_empty = !ctx_diff; + + unlink (ctx_patch1_orig_file); + free (ctx_patch1_orig_file); + unlink (patch1_new_file); + free (patch1_new_file); + + if (!delta_empty) + fuzzy_add_file (&fuzzy_delta_files, oldname + 4, + newname + 4, delta_diff); + + if (!ctx_empty) + fuzzy_add_file (&fuzzy_ctx_files, oldname + 4, + newname + 4, ctx_diff); } else { /* Write it out. */ merge_lines (&file, &file2); @@ -2322,35 +2760,18 @@ output_delta (FILE *p1, FILE *p2, FILE *out) "Error applying patch2 to reconstructed file"); } - fseek (p1, pos1, SEEK_SET); - - in = run_diff (options, tmpp1, tmpp2, &child); - diff_is_empty = !in; - - /* Rebuild the diff hunks without unlines, since fuzzy diffing shows - * context line differences that therefore may cause unlines to appear - * in the diff output. We don't want any unlines in the final output. */ - if (fuzzy && !diff_is_empty) { - in = split_patch_hunks (in, 0, NULL, NULL, file.unline); - diff_is_empty = !in; - } - - if (!diff_is_empty || ret1 || ret2) { - /* Initialize the rej pointers for output_rej_hunks() */ - struct rej_file *rej1_ptr = ret1 ? &rej1 : NULL; - struct rej_file *rej2_ptr = ret2 ? &rej2 : NULL; + if (!fuzzy && (in = run_diff (options, tmpp1, tmpp2, &child))) { /* ANOTHER temporary file! This is to catch the case * where we just don't have enough context to generate * a proper interdiff. */ FILE *tmpdiff = xtmpfile (); + int exit_err = 0; char *line = NULL; size_t linelen; - for (; !diff_is_empty;) { + for (;;) { ssize_t got = getline (&line, &linelen, in); if (got < 0) break; - /* Output fuzzy diff reject hunks in order */ - output_rej_hunks (line, &rej1_ptr, &rej2_ptr, tmpdiff); fwrite (line, (size_t) got, 1, tmpdiff); if (*line != ' ' && !strcmp (line + 1, file.unline)) { /* Uh-oh. We're trying to output a @@ -2368,16 +2789,17 @@ output_delta (FILE *p1, FILE *p2, FILE *out) * original and copy the new * version. */ fclose (tmpdiff); - free (line); - goto evasive_action; + exit_err = 1; + break; } fwrite (line, (size_t) got, 1, tmpdiff); } } free (line); - - /* Output any remaining reject hunks */ - output_rej_hunks (NULL, &rej1_ptr, &rej2_ptr, tmpdiff); + fclose (in); + waitpid (child, NULL, 0); + if (exit_err) + goto evasive_action; /* First character */ if (human_readable) { @@ -2405,23 +2827,31 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fclose (tmpdiff); } - if (in) { - fclose (in); - waitpid (child, NULL, 0); - } + /* Restore file positions for the caller's iteration loop */ + fseek (p1, pos1, SEEK_SET); + fseek (p2, pos2, SEEK_SET); + if (debug) printf ("reconstructed orig1=%s orig2=%s\n", tmpp1, tmpp2); else { unlink (tmpp1); unlink (tmpp2); if (fuzzy) { - fuzzy_cleanup (tmpp1, ret1); - fuzzy_cleanup (tmpp2, ret2); + fuzzy_cleanup (tmpp1, patch_ret); + fuzzy_cleanup (tmpp2, ctx_ret); } } free (oldname); free (newname); - clear_lines_info (&file); + if (fuzzy) { + free_lines (file.head); + free_lines (file2.head); + free (unline); + } else { + clear_lines_info (&file); + /* In non-fuzzy mode, merge_lines() transfers file2's nodes + * into file, so they're already freed above. */ + } return 0; evasive_action: @@ -2431,8 +2861,8 @@ output_delta (FILE *p1, FILE *p2, FILE *out) unlink (tmpp1); unlink (tmpp2); if (fuzzy) { - fuzzy_cleanup (tmpp1, ret1); - fuzzy_cleanup (tmpp2, ret2); + fuzzy_cleanup (tmpp1, patch_ret); + fuzzy_cleanup (tmpp2, 0); } } if (human_readable) @@ -3234,6 +3664,37 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) copy_residue (p2, mode == mode_flip ? flip1 : stdout); + /* Output the fuzzy sections after all files have been processed */ + if (fuzzy && (fuzzy_delta_files.head || fuzzy_ctx_files.head || + fuzzy_delta_rej_files.head)) { + int printed = 0; + + if (fuzzy_delta_files.head) { + fputs (DELTA_DIFF_HEADER, stdout); + fuzzy_output_list (&fuzzy_delta_files, 0, stdout); + printed = 1; + } + + if (fuzzy_delta_rej_files.head) { + if (printed) + fputc ('\n', stdout); + fputs (DELTA_REJ_HEADER, stdout); + fuzzy_output_list (&fuzzy_delta_rej_files, 1, stdout); + printed = 1; + } + + if (fuzzy_ctx_files.head) { + if (printed) + fputc ('\n', stdout); + fputs (CONTEXT_DIFF_HEADER, stdout); + fuzzy_output_list (&fuzzy_ctx_files, 0, stdout); + } + + fuzzy_free_list (&fuzzy_delta_files); + fuzzy_free_list (&fuzzy_ctx_files); + fuzzy_free_list (&fuzzy_delta_rej_files); + } + if (mode == mode_flip) { /* Now we flipped the two patches, show them. */ rewind (flip1); From e199650ea526d5c84dfbafaf760e968aad0b0f66 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 17:07:32 -0800 Subject: [PATCH 15/28] interdiff: Show files only in one patch under fuzzy mode headers In fuzzy mode, files modified by only one patch were output raw without colorization or section headers. Redirect their output into per-section tmpfile accumulators and display them under new ONLY IN PATCH1 / ONLY IN PATCH2 banners alongside the existing delta/context/rejected sections. Suppress the "reverted:" and "unchanged:" labels in fuzzy mode since the section headers already convey the meaning. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 95 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index faa581e3..eaae9bb7 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -78,6 +78,16 @@ "* CONTEXT DIFFERENCES - surrounding code differences between the patches *\n" \ "================================================================================\n\n" +#define ONLY_IN_PATCH1_HEADER \ + "================================================================================\n" \ + "* ONLY IN PATCH1 - files not modified by patch2 *\n" \ + "================================================================================\n\n" + +#define ONLY_IN_PATCH2_HEADER \ + "================================================================================\n" \ + "* ONLY IN PATCH2 - files not modified by patch1 *\n" \ + "================================================================================\n\n" + /* Line type for coloring */ enum line_type { LINE_FILE, @@ -204,6 +214,8 @@ struct fuzzy_file_list { static struct fuzzy_file_list fuzzy_delta_files = {}; static struct fuzzy_file_list fuzzy_ctx_files = {}; static struct fuzzy_file_list fuzzy_delta_rej_files = {}; +static FILE *fuzzy_only_in_patch1 = NULL; +static FILE *fuzzy_only_in_patch2 = NULL; static struct patlist *pat_drop_context = NULL; @@ -743,12 +755,12 @@ do_output_patch1_only (FILE *p1, FILE *out, int not_reverted) if (not_reverted) { /* Combinediff: copy patch */ - if (human_readable && mode != mode_flip) + if (human_readable && !fuzzy && mode != mode_flip) fprintf (out, "unchanged:\n"); fputs (oldname, out); fputs (line, out); } else if (!no_revert_omitted) { - if (human_readable) + if (human_readable && !fuzzy) fprintf (out, "reverted:\n"); fprintf (out, "--- %s", line + 4); fprintf (out, "+++ %s", oldname + 4); @@ -1402,6 +1414,36 @@ fuzzy_output_list (struct fuzzy_file_list *list, int skip_headers, FILE *out) } } +/* Colorize and output a raw diff (with --- / +++ / @@ headers) */ +static void +colorize_diff (FILE *in, FILE *out) +{ + char *line = NULL; + size_t linelen; + ssize_t got; + + rewind (in); + while ((got = getline (&line, &linelen, in)) > 0) { + enum line_type type; + + if (!strncmp (line, "--- ", 4) || !strncmp (line, "+++ ", 4)) { + type = LINE_FILE; + } else if (!strncmp (line, "@@ ", 3)) { + type = LINE_HUNK; + } else if (line[0] == '-') { + type = LINE_REMOVED; + } else if (line[0] == '+') { + type = LINE_ADDED; + } else { + fwrite (line, got, 1, out); + continue; + } + print_color (out, type, "%.*s", (int) got - 1, line); + fputc ('\n', out); + } + free (line); +} + /* `xctx` must come with `num` initialized and `s` and `len` zeroed */ static void ctx_lookbehind (const struct line_info *lines, unsigned long start_line_idx, @@ -2881,6 +2923,7 @@ copy_residue (FILE *p2, FILE *out) struct file_list *at; for (at = files_in_patch2; at; at = at->next) { + FILE *p2out; if (file_in_list (files_done, at->file) != -1) continue; @@ -2890,10 +2933,18 @@ copy_residue (FILE *p2, FILE *out) continue; fseek (p2, at->pos, SEEK_SET); - if (human_readable && mode != mode_flip) - fprintf (out, "only in patch2:\n"); - output_patch1_only (p2, out, 1); + if (fuzzy) { + if (!fuzzy_only_in_patch2) + fuzzy_only_in_patch2 = xtmpfile (); + p2out = fuzzy_only_in_patch2; + } else { + if (human_readable && mode != mode_flip) + fprintf (out, "only in patch2:\n"); + p2out = out; + } + + output_patch1_only (p2, p2out, 1); } return 0; @@ -3644,9 +3695,16 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) fseek (p1, start_pos, SEEK_SET); pos = file_in_list (files_in_patch2, p); if (pos == -1) { - output_patch1_only (p1, - mode == mode_flip ? flip2 : stdout, - mode != mode_inter); + FILE *p1out; + + if (fuzzy && mode == mode_inter) { + if (!fuzzy_only_in_patch1) + fuzzy_only_in_patch1 = xtmpfile (); + p1out = fuzzy_only_in_patch1; + } else { + p1out = mode == mode_flip ? flip2 : stdout; + } + output_patch1_only (p1, p1out, mode != mode_inter); } else { fseek (p2, pos, SEEK_SET); if (mode == mode_flip) @@ -3666,7 +3724,8 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) /* Output the fuzzy sections after all files have been processed */ if (fuzzy && (fuzzy_delta_files.head || fuzzy_ctx_files.head || - fuzzy_delta_rej_files.head)) { + fuzzy_delta_rej_files.head || fuzzy_only_in_patch1 || + fuzzy_only_in_patch2)) { int printed = 0; if (fuzzy_delta_files.head) { @@ -3688,6 +3747,24 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) fputc ('\n', stdout); fputs (CONTEXT_DIFF_HEADER, stdout); fuzzy_output_list (&fuzzy_ctx_files, 0, stdout); + printed = 1; + } + + if (fuzzy_only_in_patch1) { + if (printed) + fputc ('\n', stdout); + fputs (ONLY_IN_PATCH1_HEADER, stdout); + colorize_diff (fuzzy_only_in_patch1, stdout); + fclose (fuzzy_only_in_patch1); + printed = 1; + } + + if (fuzzy_only_in_patch2) { + if (printed) + fputc ('\n', stdout); + fputs (ONLY_IN_PATCH2_HEADER, stdout); + colorize_diff (fuzzy_only_in_patch2, stdout); + fclose (fuzzy_only_in_patch2); } fuzzy_free_list (&fuzzy_delta_files); From 8c7e18bed06dfad393da9863f62176384a7a8f69 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 17:07:37 -0800 Subject: [PATCH 16/28] interdiff: Filter spurious edge lines from context differences When comparing context differences between two patches, diff hunks can contain changes at the top or bottom edge that are exclusively additions or exclusively deletions. These are not real differences -- they are artifacts of one patch having captured more or fewer context lines than the other around the same code change. For example, if patch2 includes 5 lines of context above a change but patch1 only includes 3, the context diff will show those 2 extra lines as additions at the top of a hunk. This is misleading because the patches make the same change; they just differ in how much surrounding code was captured. Add filter_edge_hunks() to detect and handle these spurious edge lines. For each hunk, the first and last context lines partition the body into three regions: top edge, middle, and bottom edge. Each edge is then classified: - Two-sided (has both additions and deletions): a real change, kept as-is - One-sided (exclusively additions or exclusively deletions): spurious, trimmed from the hunk and the @@ header line counts adjusted If no changes remain after trimming (the entire hunk was spurious edges), the hunk is dropped. If all hunks are dropped, the CONTEXT DIFFERENCES section is suppressed entirely. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/interdiff.c b/src/interdiff.c index eaae9bb7..1f7ecef1 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -2539,6 +2539,125 @@ filter_inverted_rejects (FILE *delta, FILE *rej) return out; } +/* Filter out spurious edge lines from context diff hunks. Changes that are + * exclusively additions or exclusively deletions at the top or bottom edge of a + * hunk are artifacts of one patch capturing more context lines than the other. + * Hunks composed entirely of such edges are dropped. Hunks with real changes in + * the middle have their additions-only or deletions-only edges trimmed and the + * @@ header line counts adjusted accordingly. + * + * Closes the input file. Returns NULL if nothing remains. */ +static FILE * +filter_edge_hunks (FILE *in) +{ + struct line_info atat, *lines = NULL; + char *fbuf, *end, *line; + size_t nlines = 0, fsz; + FILE *out = NULL; + + /* Read entire input into a buffer */ + fseek (in, 0, SEEK_END); + fsz = ftell (in); + fbuf = xmalloc (fsz); + rewind (in); + if (fread (fbuf, 1, fsz, in) != fsz) + error (EXIT_FAILURE, errno, "fread() fail"); + fclose (in); + + end = fbuf + fsz; + atat = (typeof (atat)){ fbuf }; /* The first line is the @@ line */ + for (line = fbuf; (line = memchr (line, '\n', end - line));) { + /* ntop/nbot[0] = deleted, [1] = added edge lines */ + int ntop[2] = {}, nbot[2] = {}; + size_t first_ctx, last_ctx = 0, from = 0, to; + + /* Set the previous line length, advancing `line` past '\n' */ + if (atat.len) + lines[nlines - 1].len = ++line - lines[nlines - 1].s; + else + atat.len = ++line - atat.s; + + /* Accumulate non-@@ lines into the current hunk. At EOF, + * line == end so we fall through to process the last hunk. */ + if (line < end && strncmp (line, "@@ ", 3)) { + lines = xrealloc (lines, (nlines + 1) * sizeof (*lines)); + lines[nlines++].s = line; + continue; + } + + first_ctx = to = nlines; + + /* Process accumulated hunk on new @@ or final line (no-op when + * nlines == 0 since all loop ranges are empty). Find first and + * last context lines. */ + for (size_t i = 0; i < nlines; i++) { + if (lines[i].s[0] == ' ') { + if (first_ctx == nlines) + first_ctx = i; + last_ctx = i; + } + } + + /* Count top edge +/- lines (before first context) */ + for (size_t i = 0; i < first_ctx; i++) + ntop[lines[i].s[0] == '+']++; + + /* Count bottom edge +/- lines (after last context) */ + for (size_t i = last_ctx + 1; i < nlines; i++) + nbot[lines[i].s[0] == '+']++; + + /* Trim one-sided edges; reset counts for two-sided edges */ + if (ntop[0] && ntop[1]) + ntop[0] = ntop[1] = 0; + else if (ntop[0] || ntop[1]) + from = first_ctx; + if (nbot[0] && nbot[1]) + nbot[0] = nbot[1] = 0; + else if (nbot[0] || nbot[1]) + to = last_ctx + 1; + + /* Write hunk if remaining lines have changes */ + for (size_t i = from; i < to; i++) { + if (lines[i].s[0] == ' ') + continue; + + if (!out) + out = xtmpfile (); + + if (from || to < nlines) { + /* Edges were trimmed; regenerate the @@ header + * with adjusted line counts. sscanf is fine + * instead of read_atatline because the input + * comes directly from diff and is always + * uniformly formatted. */ + int ostart, ocount, nstart, ncount; + sscanf (atat.s, "@@ -%d,%d +%d,%d @@", + &ostart, &ocount, &nstart, &ncount); + fprintf (out, "@@ -%d,%d +%d,%d @@\n", + ostart + ntop[0], + ocount - ntop[0] - nbot[0], + nstart + ntop[1], + ncount - ntop[1] - nbot[1]); + } else { + fwrite (atat.s, atat.len, 1, out); + } + for (size_t j = from; j < to; j++) + fwrite (lines[j].s, lines[j].len, 1, out); + break; + } + + /* Reset for the next hunk */ + nlines = 0; + atat = (typeof (atat)){ line }; + } + free (lines); + free (fbuf); + + if (out) + rewind (out); + return out; +} + /* Run diff and filter out bogus hunks containing unlines. * * Returns NULL if the resulting diff is empty. */ @@ -2773,6 +2892,8 @@ output_delta (FILE *p1, FILE *p2, FILE *out) ctx_diff = run_and_clean_diff (options, ctx_patch1_orig_file, tmpp2, unline); + if (ctx_diff) + ctx_diff = filter_edge_hunks (ctx_diff); ctx_empty = !ctx_diff; unlink (ctx_patch1_orig_file); From 7df90d192669e1719020b118d3e51b9095dd5b1d Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 19 Feb 2026 14:59:30 -0800 Subject: [PATCH 17/28] interdiff: Add get_fuzz() helper function Extract the fuzz value calculation from apply_patch() into a standalone helper so it can be reused by split_patch_hunks(). Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 1f7ecef1..7e96dfd7 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1058,6 +1058,18 @@ open_rej_file (const char *file, struct rej_file *rej) fseek (rej->fp, atat_pos, SEEK_SET); } +static int +get_fuzz (void) +{ + if (max_fuzz_user >= 0) + return max_fuzz_user; + + if (max_context) + return max_context - 1; + + return 0; +} + static int apply_patch (FILE *patch, const char *file, int reverted, struct rej_file *rej, FILE **out) @@ -1094,18 +1106,10 @@ apply_patch (FILE *patch, const char *file, int reverted, struct rej_file *rej, argv[argc++] = reverted ? (has_ignore_all_space ? "-NRlp0" : "-NRp0") : (has_ignore_all_space ? "-Nlp0" : "-Np0"); if (fuzzy) { - int fuzz = 0; - /* Don't generate .orig files when we expect rejected hunks */ argv[argc++] = "--no-backup-if-mismatch"; - /* Either pass in the user-supplied max fuzz, or fuzz all but - * one pre-context and one post-context line by default. */ - if (max_fuzz_user >= 0) - fuzz = max_fuzz_user; - else if (max_context) - fuzz = max_context - 1; - if (asprintf (&fuzz_arg, "--fuzz=%d", fuzz) < 0) + if (asprintf (&fuzz_arg, "--fuzz=%d", get_fuzz ()) < 0) error (EXIT_FAILURE, errno, "asprintf failed"); argv[argc++] = fuzz_arg; } From f1cbb0662f432ef51356bef6617595060c1ac5d1 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Sun, 22 Feb 2026 20:37:42 -0800 Subject: [PATCH 18/28] interdiff: Fix crash in ctx_lookahead() on trailing "+" lines When the last line in the remaining hunk content is a "+" line, ctx_lookahead() skips it without checking to see if it's the last line in the hunk. The loop then continues with a NULL next_line pointer, crashing on dereference. Fix it by checking for end-of-hunk even after "+" lines. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 7e96dfd7..b9466b25 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1496,21 +1496,20 @@ ctx_lookahead (const char *hunk, size_t hlen, struct xtra_context *xctx) /* Get the next line now to find the length of the line */ next_line = memchr (line, '\n', hunk + hlen - line); - if (*line == '+') - continue; - - linelen = next_line + 1 - line; - - /* Copy out the line and ensure the first character is a space, - * since it may be a minus. */ - xctx->s = xrealloc (xctx->s, xctx->len + linelen); - memcpy (xctx->s + xctx->len, line, linelen); - xctx->s[xctx->len] = ' '; - xctx->len += linelen; - - /* Quit when we've got the desired number of context lines */ - if (++num == xctx->num) - break; + if (*line != '+') { + linelen = next_line + 1 - line; + + /* Copy out the line and ensure the first character is a + * space, since it may be a minus. */ + xctx->s = xrealloc (xctx->s, xctx->len + linelen); + memcpy (xctx->s + xctx->len, line, linelen); + xctx->s[xctx->len] = ' '; + xctx->len += linelen; + + /* Quit when we've got the desired amount of context */ + if (++num == xctx->num) + break; + } /* Stop when this is the end of the hunk, recording the actual * number of extra context lines found. */ From 9ade8a2f6c31ca4c549608f25205b71d5eeed091 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 17:29:22 -0800 Subject: [PATCH 19/28] interdiff: Fix split hunk context imbalance causing patch rejections The patch utility distributes fuzz across both sides of a hunk's context by computing prefix_fuzz and suffix_fuzz from the fuzz value and the number of pre/post context lines. When the two sides are unbalanced, the larger side consumes the entire fuzz budget and the smaller side gets none -- any context mismatch on the short side causes the hunk to be rejected outright. For example, a split sub-hunk with 3 lines of pre-context and 6 lines of post-context would have prefix_fuzz = fuzz + 3 - 6, which is zero (or negative, clamped to zero) at every fuzz level. Even a single mismatched pre-context line will cause the hunk to fail, despite the mismatch being well within the configured fuzz limit. In the opposite direction, when prefix exceeds suffix, the patch utility computes a negative suffix_fuzz. Unlike prefix_fuzz (which is clamped to zero), a negative suffix_fuzz restricts matching to end-of-file only, causing mid-file hunks to be rejected even when the context is otherwise identical. Both imbalances arise because split_patch_hunks() shares the gap context lines between consecutive sub-hunks: the gap appears as the post-context of the first sub-hunk and the pre-context of the second. When the gap is larger or smaller than the original hunk's leading or trailing context, one side ends up with more context than the other. Fix by: - Widening the xctx_post condition to add extra post-context whenever total prefix exceeds suffix, not just when post-context is below nctx_target. - Clamping xctx_pre propagation at the fuzz value to prevent cascading prefix inflation across sub-hunks. - Capping the next sub-hunk's normal pre-context (nctx[0]) at max_context so the fuzz budget stays balanced even for the last sub-hunk where ctx_lookahead cannot add extra post-context. - When suffix > prefix, keeping at least 1 post-context line and updating nctx[1] after trimming so the next sub-hunk's overlap is correct. - Trimming excess pre-context at end-of-file when suffix_fuzz would go negative and ctx_lookahead cannot add extra post-context. - Skipping the overlap entirely when ostart would underflow below 1 (hunks at the start of a file have no preceding lines to overlap with). Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 109 +++++++++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index b9466b25..14329546 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1624,7 +1624,6 @@ split_patch_hunks (FILE *patch, size_t len, char *file, * ndelta[0] = deleted lines, ndelta[1] = added lines */ unsigned long nctx[2] = {}, ndelta[2] = {}; unsigned long ostart, nstart, orig_nstart, start_line_idx = 0; - unsigned long nctx_target = 0; /* Init for spurious GCC warn */ struct xtra_context xctx_pre = {}; struct line_info *lines = NULL; unsigned long num_lines = 0; @@ -1646,21 +1645,6 @@ split_patch_hunks (FILE *patch, size_t len, char *file, else hlen = strlen (hunk); - /* Count the number of pre-context and post-context lines in - * this hunk. The greater of the two will be the number of pre- - * context and post-context lines targeted per split hunk. */ - if (!unline) { - unsigned long orig_hunk_nctx[2] = {}; - - for (line = hunk; - (line = memchr (line, '\n', hunk + hlen - line)) && - line[1] == ' '; line++, orig_hunk_nctx[0]++); - for (line = hunk + hlen - 1; - (line = memrchr (hunk, '\n', line - hunk)) && - line[1] == ' '; line--, orig_hunk_nctx[1]++); - nctx_target = MAX (orig_hunk_nctx[0], orig_hunk_nctx[1]); - } - /* Split this hunk into multiple smaller hunks, if possible. * This is done by looking for deltas (+/- lines) that aren't * contiguous and thus have context lines in between them. Note @@ -1803,17 +1787,55 @@ split_patch_hunks (FILE *patch, size_t len, char *file, /* Add the start offset to the old/new lines */ ostart += start_off; nstart += start_off; - } else if (nctx[1] < nctx_target && hlen_rem) { - /* If the number of post-context lines is still - * below the target number afterwards, then it - * means we hit the end of the original hunk - * itself. It's technically fine because it - * means the original hunk came with an unequal - * number of pre- and post-context lines. */ - xctx_post.num = nctx_target - nctx[1]; + } else if (hlen_rem && nctx[1] < nctx[0] + xctx_pre.num) { + /* Ensure post-context is at least as large as + * pre-context to avoid a negative suffix_fuzz + * in the patch utility, which would restrict + * matching to end-of-file. */ + xctx_post.num = nctx[0] + xctx_pre.num - nctx[1]; ctx_lookahead (line, hlen_rem, &xctx_post); } + /* Cap post-context at the pre-context count so the fuzz + * budget is evenly split. The patch utility distributes + * fuzz as prefix_fuzz = fuzz + prefix - context, so when + * suffix > prefix, all the fuzz goes to the suffix and + * prefix mismatches can't be fuzzed at all. */ + if (!unline) { + unsigned long prefix = nctx[0] + xctx_pre.num; + unsigned long suffix = nctx[1] + xctx_post.num; + int fuzz = get_fuzz (); + + if (suffix > prefix) { + unsigned long trim = suffix - prefix; + + /* Keep at least 1 context line so + * the patch utility can anchor the + * hunk to the correct position. */ + if (trim >= nctx[1]) + trim = nctx[1] ? nctx[1] - 1 + : 0; + end_line -= trim; + nctx[1] -= trim; + } else if (prefix > suffix + fuzz && + nctx[0]) { + /* Trim excess pre-context when the + * suffix_fuzz would go negative in + * the patch utility. This handles + * the last sub-hunk at end-of-file + * where ctx_lookahead cannot help. */ + unsigned long trim = + prefix - suffix - fuzz; + if (trim > nctx[0]) + trim = nctx[0]; + start_line += trim; + start_line_idx += trim; + ostart += trim; + nstart += trim; + nctx[0] -= trim; + } + } + /* Calculate the old and new line counts */ onum = nnum = xctx_pre.num + /* Extra pre-context */ end_line + 1 - start_line + /* Hunk */ @@ -1863,21 +1885,32 @@ split_patch_hunks (FILE *patch, size_t len, char *file, /* Find extra pre-context if extra post-context * was used for this split hunk, since it means * that there isn't enough normal post-context - * to be the next split hunk's pre-context. */ - start_line_idx -= 1 + nctx[1]; - xctx_pre = (typeof (xctx_pre)){ xctx_post.num }; - if (xctx_pre.num) - ctx_lookbehind (lines, start_line_idx, + * to be the next split hunk's pre-context. + * Clamp both nctx[0] and xctx_pre so the next + * hunk's prefix never causes a negative + * suffix_fuzz in the patch utility. */ + nctx[0] = MIN (nctx[1], max_context); + + /* If the overlap would push ostart below 1, + * skip it — there are no lines before the + * start of the file to overlap with. */ + if (xctx_post.num + nctx[0] >= ostart) { + nctx[0] = 0; + start_line_idx -= 1; + } else { + start_line_idx -= 1 + nctx[0]; + xctx_pre = (typeof (xctx_pre)) + { MIN (xctx_post.num, + get_fuzz ()) }; + if (xctx_pre.num) + ctx_lookbehind (lines, + start_line_idx, &xctx_pre); - - /* Subtract the extra post-context lines of this - * hunk, the normal post-context lines of this - * hunk, and the extra pre-context lines for the - * _next_ hunk to get the _next_ hunk's starting - * line numbers. */ - ostart -= xctx_pre.num + xctx_post.num + nctx[1]; - nstart -= xctx_pre.num + xctx_post.num + nctx[1]; - nctx[0] = nctx[1]; + ostart -= xctx_pre.num + + xctx_post.num + nctx[0]; + nstart -= xctx_pre.num + + xctx_post.num + nctx[0]; + } nctx[1] = 0; ndelta[1] = *line == '+'; ndelta[0] = !ndelta[1]; From abc4b539376d47800ef5ee39611333ceed5b9eeb Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 17:18:00 -0800 Subject: [PATCH 20/28] Rename README.md to README On automake versions <1.16.4, "README.md" isn't supported, causing automake to fail. To maintain compatibility with older automake versions, rename README.md to README, which is supported on old and new versions of automake alike. --- README.md => README | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.md => README (100%) diff --git a/README.md b/README similarity index 100% rename from README.md rename to README From 4f6f12f4764ecef42429d030d320fd96d95fcf48 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 17:27:23 -0800 Subject: [PATCH 21/28] Update README build instructions and add testing section Update the build-from-git instructions to include the gnulib-update.sh and bootstrap steps, and add a section explaining how to run the test suite. Assisted-by: Claude Opus 4.6 (1M context) --- README | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README b/README index eeb7162b..bc2f2968 100644 --- a/README +++ b/README @@ -57,20 +57,39 @@ Patchutils is a small collection of programs that operate on patch files. It pro - **svndiff** / **svndiffview** - Subversion-specific diff viewing tools. -## Installation - -Patchutils uses the standard GNU autotools build system: +## Building from the Git repository ```bash +./gnulib-update.sh +./bootstrap ./configure -make +make -j$(nproc) +``` + +The `gnulib-update.sh` and `./bootstrap` steps are only needed once +(or when `configure.ac` / `Makefile.am` change). After that, just +`./configure && make` is sufficient. + +This requires **automake**, **autoconf**, and **gnulib-devel** to be +installed on your system. + +## Installation + +```bash make install ``` -## Building from the Git repository +## Testing + +```bash +make check +``` + +Run a single test: -After cloning the source from GitHub, run `./bootstrap` to generate the `configure` script. -This step requires **automake**, **autoconf**, and **gnulib-devel** to be installed on your system. +```bash +make check TESTS=tests/addhunk1/run-test +``` ## Requirements From 626b7ec2bdf2ee4a208a2690b6fab8f5231da29f Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 17:27:45 -0800 Subject: [PATCH 22/28] interdiff: Handle '\ No newline' markers in split_patch_hunks The '\ No newline at end of file' marker starts with '\', which is not '+', '-', or ' '. Without special handling, it corrupts the hunk structure in split_patch_hunks(). Skip these markers for counting purposes but extend the previous line's length to cover the marker, so it appears in the split output and the patch utility handles it. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/interdiff.c b/src/interdiff.c index 14329546..7a5d2d48 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1735,6 +1735,22 @@ split_patch_hunks (FILE *patch, size_t len, char *file, if (!hlen_rem) goto split_hunk_incl_latest; + /* Skip '\ No newline at end of file' markers for + * counting purposes but include them in the output + * by extending the previous line's length to cover + * the marker. */ + if (*line == '\\') { + if (lines) { + char *eol = memchr (line, '\n', + hunk + hlen - + line); + lines[num_lines - 1].len = + (eol ? eol + 1 : hunk + hlen) + - lines[num_lines - 1].s; + } + continue; + } + /* Record the current line, setting `len` to zero */ lines = xrealloc (lines, ++num_lines * sizeof (*lines)); lines[num_lines - 1] = (typeof (*lines)){ line }; From 2c2f30df60095000c31763028b6093b68cd0ca7b Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 16:22:36 -0800 Subject: [PATCH 23/28] interdiff: Fix newline stripping for reverted patches in create_orig The last_was_add variable was checked against line[0] (the original character) instead of first_char (the reverted character). When processing a reverted patch, a '-' line that becomes '+' would set last_was_add to false, causing the '\ No newline' handler to incorrectly strip the trailing newline from the preceding context line. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interdiff.c b/src/interdiff.c index 7a5d2d48..7287fbaa 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -624,7 +624,7 @@ create_orig (FILE *f, struct lines_info *file, break; } - last_was_add = (line[0] == '+'); + last_was_add = (first_char == '+'); } if (!newline) { From 4793d243041fed8eda2bb63a0b71d5fc01aeb11b Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 16:22:42 -0800 Subject: [PATCH 24/28] interdiff: Skip fuzzy processing for file creations and deletions When one side of a diff is /dev/null (file creation or deletion), the fuzzy path has no content to reconstruct and diff against. Skip to cleanup to avoid spurious rejected hunks from deletion diffs. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/interdiff.c b/src/interdiff.c index 7287fbaa..db383c4f 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -2846,6 +2846,13 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fseek (p1, start1, SEEK_SET); fseek (p2, start2, SEEK_SET); + /* Skip fuzzy processing for file creations/deletions. One + * side is /dev/null, so there's no content to diff. */ + if (fuzzy && + (!strcmp (oldname + 4, "/dev/null") || + !strcmp (newname + 4, "/dev/null"))) + goto fuzzy_skip; + if (fuzzy) { unsigned long *hunk_offs = NULL, *ctx_hunk_offs = NULL; char *patch1_new_file, *ctx_patch1_orig_file; @@ -3056,6 +3063,7 @@ output_delta (FILE *p1, FILE *p2, FILE *out) fuzzy_cleanup (tmpp2, ctx_ret); } } + fuzzy_skip: free (oldname); free (newname); if (fuzzy) { From edded43c62da12d4c2294f6a249c9f5aa0add69d Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 16:22:49 -0800 Subject: [PATCH 25/28] interdiff: Suppress false 'doesn't contain a patch' for non-diff patches Patches that contain only mode changes, binary diffs, merge metadata, or no diff content at all would trigger a spurious error message. Check for actual file-header '--- a/' or '--- /dev/null' lines before reporting a patch as invalid. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index db383c4f..7f02a827 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -3229,10 +3229,26 @@ index_patch_generic (FILE *patch_file, struct file_list **file_list, int need_sk if (line) free (line); + /* Return success if the file is empty, has indexed files, or + * has no --- lines at all (merge commits, mode-only changes, + * notes-only commits, binary-only patches, etc.). */ if (file_is_empty || *file_list) return 0; - else - return 1; + + /* Check if the patch has any file-header --- lines (as + * opposed to commit-message separators or b4 tracking). + * If not, it simply has no diffable content. */ + line = NULL; + linelen = 0; + rewind (patch_file); + while (getline (&line, &linelen, patch_file) != -1) + if (!strncmp (line, "--- a/", 6) || + !strncmp (line, "--- /dev/null", 13)) { + free (line); + return 1; + } + free (line); + return 0; } static int @@ -3898,8 +3914,21 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) free (p); } - if (!file_is_empty && !patch_found) - no_patch (patch1); + if (!file_is_empty && !patch_found) { + /* Don't warn for patches with no file-header --- lines + * (merge commits, mode-only, binary-only, etc.). */ + int has_diff_content = 0; + + rewind (p1); + while (getline (&line, &linelen, p1) != -1) + if (!strncmp (line, "--- a/", 6) || + !strncmp (line, "--- /dev/null", 13)) { + has_diff_content = 1; + break; + } + if (has_diff_content) + no_patch (patch1); + } copy_residue (p2, mode == mode_flip ? flip1 : stdout); From 70404ee97c3de3675c4d6ef788359072c4388512 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 16:35:04 -0800 Subject: [PATCH 26/28] tests: Add fuzzy mode tests for interdiff Add comprehensive test coverage for the fuzzy diffing mode: - fuzzy1-10: Core fuzzy diffing scenarios including context differences, rejected hunks, real-world kernel patches, and edge cases. - fuzzy11: '\ No newline at end of file' marker handling. - fuzzy12: Addition at line 1 with zero pre-context. - fuzzy13: File deletion (--- a/file, +++ /dev/null) handling. - fuzzy14: Mode-only patches with no diff hunks. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile.am | 16 +- tests/fuzzy1/run-test | 91 +++ tests/fuzzy10/run-test | 504 ++++++++++++ tests/fuzzy11/run-test | 29 + tests/fuzzy12/run-test | 20 + tests/fuzzy13/run-test | 40 + tests/fuzzy14/run-test | 35 + tests/fuzzy2/run-test | 42 + tests/fuzzy3/run-test | 55 ++ tests/fuzzy4/run-test | 89 +++ tests/fuzzy5/run-test | 126 +++ tests/fuzzy6/run-test | 55 ++ tests/fuzzy7/run-test | 918 +++++++++++++++++++++ tests/fuzzy8/run-test | 1735 ++++++++++++++++++++++++++++++++++++++++ tests/fuzzy9/run-test | 293 +++++++ 15 files changed, 4047 insertions(+), 1 deletion(-) create mode 100755 tests/fuzzy1/run-test create mode 100755 tests/fuzzy10/run-test create mode 100755 tests/fuzzy11/run-test create mode 100755 tests/fuzzy12/run-test create mode 100755 tests/fuzzy13/run-test create mode 100755 tests/fuzzy14/run-test create mode 100755 tests/fuzzy2/run-test create mode 100755 tests/fuzzy3/run-test create mode 100755 tests/fuzzy4/run-test create mode 100755 tests/fuzzy5/run-test create mode 100755 tests/fuzzy6/run-test create mode 100755 tests/fuzzy7/run-test create mode 100755 tests/fuzzy8/run-test create mode 100755 tests/fuzzy9/run-test diff --git a/Makefile.am b/Makefile.am index 2214bf97..db7aabb7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -432,7 +432,21 @@ TESTS = tests/newline1/run-test \ tests/git-deleted-file/run-test \ tests/git-pure-rename/run-test \ tests/git-diff-edge-cases/run-test \ - tests/malformed-diff-headers/run-test + tests/malformed-diff-headers/run-test \ + tests/fuzzy1/run-test \ + tests/fuzzy2/run-test \ + tests/fuzzy3/run-test \ + tests/fuzzy4/run-test \ + tests/fuzzy5/run-test \ + tests/fuzzy6/run-test \ + tests/fuzzy7/run-test \ + tests/fuzzy8/run-test \ + tests/fuzzy9/run-test \ + tests/fuzzy10/run-test \ + tests/fuzzy11/run-test \ + tests/fuzzy12/run-test \ + tests/fuzzy13/run-test \ + tests/fuzzy14/run-test # Scanner tests (only when scanner-patchfilter is enabled) if USE_SCANNER_PATCHFILTER diff --git a/tests/fuzzy1/run-test b/tests/fuzzy1/run-test new file mode 100755 index 00000000..8dce9c87 --- /dev/null +++ b/tests/fuzzy1/run-test @@ -0,0 +1,91 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with one rejected hunk per patched file. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file +@@ -1,4 +1,4 @@ +-line 1 ++LINE 1 + line 2 + line 3 + line 4 +EOF + +cat << 'EOF' > patch2 +--- file ++++ file +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- file ++++ file +@@ -1,4 +1,4 @@ +-LINE 1 ++line 1 + line 2 + line 3 + line 4 + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- file ++++ file +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- file ++++ file +@@ -1,4 +1,9 @@ +-line 1 +-line 2 +-line 3 +-line 4 ++line 5 ++if ++1 ++fi ++if ++2 ++fi ++A ++B +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy10/run-test b/tests/fuzzy10/run-test new file mode 100755 index 00000000..bfab2af7 --- /dev/null +++ b/tests/fuzzy10/run-test @@ -0,0 +1,504 @@ +#\!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with a Linux kernel backport (libceph) that moves a +# function to a different location. The two patches are identical except for +# two context line differences and different line numbers. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +From d68ec55448b2bcf3025a29dfaa3d591b8ed51dea Mon Sep 17 00:00:00 2001 +From: Brett Mastbergen +Date: Wed, 18 Feb 2026 13:52:58 -0500 +Subject: [PATCH] libceph: fix potential use-after-free in + have_mon_and_osd_map() + +jira VULN-170008 +cve CVE-2025-68285 +commit-author Ilya Dryomov +commit 076381c261374c587700b3accf410bdd2dba334e + +The wait loop in __ceph_open_session() can race with the client +receiving a new monmap or osdmap shortly after the initial map is +received. Both ceph_monc_handle_map() and handle_one_map() install +a new map immediately after freeing the old one + + kfree(monc->monmap); + monc->monmap = monmap; + + ceph_osdmap_destroy(osdc->osdmap); + osdc->osdmap = newmap; + +under client->monc.mutex and client->osdc.lock respectively, but +because neither is taken in have_mon_and_osd_map() it's possible for +client->monc.monmap->epoch and client->osdc.osdmap->epoch arms in + + client->monc.monmap && client->monc.monmap->epoch && + client->osdc.osdmap && client->osdc.osdmap->epoch; + +condition to dereference an already freed map. This happens to be +reproducible with generic/395 and generic/397 with KASAN enabled: + + BUG: KASAN: slab-use-after-free in have_mon_and_osd_map+0x56/0x70 + Read of size 4 at addr ffff88811012d810 by task mount.ceph/13305 + CPU: 2 UID: 0 PID: 13305 Comm: mount.ceph Not tainted 6.14.0-rc2-build2+ #1266 + ... + Call Trace: + + have_mon_and_osd_map+0x56/0x70 + ceph_open_session+0x182/0x290 + ceph_get_tree+0x333/0x680 + vfs_get_tree+0x49/0x180 + do_new_mount+0x1a3/0x2d0 + path_mount+0x6dd/0x730 + do_mount+0x99/0xe0 + __do_sys_mount+0x141/0x180 + do_syscall_64+0x9f/0x100 + entry_SYSCALL_64_after_hwframe+0x76/0x7e + + + Allocated by task 13305: + ceph_osdmap_alloc+0x16/0x130 + ceph_osdc_init+0x27a/0x4c0 + ceph_create_client+0x153/0x190 + create_fs_client+0x50/0x2a0 + ceph_get_tree+0xff/0x680 + vfs_get_tree+0x49/0x180 + do_new_mount+0x1a3/0x2d0 + path_mount+0x6dd/0x730 + do_mount+0x99/0xe0 + __do_sys_mount+0x141/0x180 + do_syscall_64+0x9f/0x100 + entry_SYSCALL_64_after_hwframe+0x76/0x7e + + Freed by task 9475: + kfree+0x212/0x290 + handle_one_map+0x23c/0x3b0 + ceph_osdc_handle_map+0x3c9/0x590 + mon_dispatch+0x655/0x6f0 + ceph_con_process_message+0xc3/0xe0 + ceph_con_v1_try_read+0x614/0x760 + ceph_con_workfn+0x2de/0x650 + process_one_work+0x486/0x7c0 + process_scheduled_works+0x73/0x90 + worker_thread+0x1c8/0x2a0 + kthread+0x2ec/0x300 + ret_from_fork+0x24/0x40 + ret_from_fork_asm+0x1a/0x30 + +Rewrite the wait loop to check the above condition directly with +client->monc.mutex and client->osdc.lock taken as appropriate. While +at it, improve the timeout handling (previously mount_timeout could be +exceeded in case wait_event_interruptible_timeout() slept more than +once) and access client->auth_err under client->monc.mutex to match +how it's set in finish_auth(). + +monmap_show() and osdmap_show() now take the respective lock before +accessing the map as well. + + Cc: stable@vger.kernel.org + Reported-by: David Howells + Signed-off-by: Ilya Dryomov + Reviewed-by: Viacheslav Dubeyko +(cherry picked from commit 076381c261374c587700b3accf410bdd2dba334e) + Signed-off-by: Brett Mastbergen +--- + net/ceph/ceph_common.c | 53 +++++++++++++++++++++++++----------------- + net/ceph/debugfs.c | 14 +++++++---- + 2 files changed, 42 insertions(+), 25 deletions(-) + +diff --git a/net/ceph/ceph_common.c b/net/ceph/ceph_common.c +index 91d7783eee7df..47b3aa42b21d3 100644 +--- a/net/ceph/ceph_common.c ++++ b/net/ceph/ceph_common.c +@@ -681,42 +681,53 @@ void ceph_destroy_client(struct ceph_client *client) + } + EXPORT_SYMBOL(ceph_destroy_client); + +-/* +- * true if we have the mon map (and have thus joined the cluster) +- */ +-static bool have_mon_and_osd_map(struct ceph_client *client) +-{ +- return client->monc.monmap && client->monc.monmap->epoch && +- client->osdc.osdmap && client->osdc.osdmap->epoch; +-} +- + /* + * mount: join the ceph cluster, and open root directory. + */ + int __ceph_open_session(struct ceph_client *client, unsigned long started) + { +- unsigned long timeout = client->options->mount_timeout; +- long err; ++ DEFINE_WAIT_FUNC(wait, woken_wake_function); ++ long timeout = ceph_timeout_jiffies(client->options->mount_timeout); ++ bool have_monmap, have_osdmap; ++ int err; + + /* open session, and wait for mon and osd maps */ + err = ceph_monc_open_session(&client->monc); + if (err < 0) + return err; + +- while (!have_mon_and_osd_map(client)) { +- if (timeout && time_after_eq(jiffies, started + timeout)) +- return -ETIMEDOUT; ++ add_wait_queue(&client->auth_wq, &wait); ++ for (;;) { ++ mutex_lock(&client->monc.mutex); ++ err = client->auth_err; ++ have_monmap = client->monc.monmap && client->monc.monmap->epoch; ++ mutex_unlock(&client->monc.mutex); ++ ++ down_read(&client->osdc.lock); ++ have_osdmap = client->osdc.osdmap && client->osdc.osdmap->epoch; ++ up_read(&client->osdc.lock); ++ ++ if (err || (have_monmap && have_osdmap)) ++ break; ++ ++ if (signal_pending(current)) { ++ err = -ERESTARTSYS; ++ break; ++ } ++ ++ if (!timeout) { ++ err = -ETIMEDOUT; ++ break; ++ } + + /* wait */ + dout("mount waiting for mon_map\n"); +- err = wait_event_interruptible_timeout(client->auth_wq, +- have_mon_and_osd_map(client) || (client->auth_err < 0), +- ceph_timeout_jiffies(timeout)); +- if (err < 0) +- return err; +- if (client->auth_err < 0) +- return client->auth_err; ++ timeout = wait_woken(&wait, TASK_INTERRUPTIBLE, timeout); + } ++ remove_wait_queue(&client->auth_wq, &wait); ++ ++ if (err) ++ return err; + + pr_info("client%llu fsid %pU\n", ceph_client_gid(client), + &client->fsid); +diff --git a/net/ceph/debugfs.c b/net/ceph/debugfs.c +index 009767c3bcaa4..9f3540a1f7d8b 100644 +--- a/net/ceph/debugfs.c ++++ b/net/ceph/debugfs.c +@@ -35,8 +35,9 @@ static int monmap_show(struct seq_file *s, void *p) + int i; + struct ceph_client *client = s->private; + ++ mutex_lock(&client->monc.mutex); + if (client->monc.monmap == NULL) +- return 0; ++ goto out_unlock; + + seq_printf(s, "epoch %d\n", client->monc.monmap->epoch); + for (i = 0; i < client->monc.monmap->num_mon; i++) { +@@ -47,6 +48,9 @@ static int monmap_show(struct seq_file *s, void *p) + ENTITY_NAME(inst->name), + ceph_pr_addr(&inst->addr.in_addr)); + } ++ ++out_unlock: ++ mutex_unlock(&client->monc.mutex); + return 0; + } + +@@ -55,13 +59,14 @@ static int osdmap_show(struct seq_file *s, void *p) + int i; + struct ceph_client *client = s->private; + struct ceph_osd_client *osdc = &client->osdc; +- struct ceph_osdmap *map = osdc->osdmap; ++ struct ceph_osdmap *map; + struct rb_node *n; + ++ down_read(&osdc->lock); ++ map = osdc->osdmap; + if (map == NULL) +- return 0; ++ goto out_unlock; + +- down_read(&osdc->lock); + seq_printf(s, "epoch %u barrier %u flags 0x%x\n", map->epoch, + osdc->epoch_barrier, map->flags); + +@@ -128,6 +133,7 @@ static int osdmap_show(struct seq_file *s, void *p) + seq_printf(s, "]\n"); + } + ++out_unlock: + up_read(&osdc->lock); + return 0; + } +-- +2.53.0 + +EOF + +cat << 'EOF' > patch2 +From 076381c261374c587700b3accf410bdd2dba334e Mon Sep 17 00:00:00 2001 +From: Ilya Dryomov +Date: Mon, 3 Nov 2025 21:34:01 +0100 +Subject: [PATCH] libceph: fix potential use-after-free in + have_mon_and_osd_map() + +The wait loop in __ceph_open_session() can race with the client +receiving a new monmap or osdmap shortly after the initial map is +received. Both ceph_monc_handle_map() and handle_one_map() install +a new map immediately after freeing the old one + + kfree(monc->monmap); + monc->monmap = monmap; + + ceph_osdmap_destroy(osdc->osdmap); + osdc->osdmap = newmap; + +under client->monc.mutex and client->osdc.lock respectively, but +because neither is taken in have_mon_and_osd_map() it's possible for +client->monc.monmap->epoch and client->osdc.osdmap->epoch arms in + + client->monc.monmap && client->monc.monmap->epoch && + client->osdc.osdmap && client->osdc.osdmap->epoch; + +condition to dereference an already freed map. This happens to be +reproducible with generic/395 and generic/397 with KASAN enabled: + + BUG: KASAN: slab-use-after-free in have_mon_and_osd_map+0x56/0x70 + Read of size 4 at addr ffff88811012d810 by task mount.ceph/13305 + CPU: 2 UID: 0 PID: 13305 Comm: mount.ceph Not tainted 6.14.0-rc2-build2+ #1266 + ... + Call Trace: + + have_mon_and_osd_map+0x56/0x70 + ceph_open_session+0x182/0x290 + ceph_get_tree+0x333/0x680 + vfs_get_tree+0x49/0x180 + do_new_mount+0x1a3/0x2d0 + path_mount+0x6dd/0x730 + do_mount+0x99/0xe0 + __do_sys_mount+0x141/0x180 + do_syscall_64+0x9f/0x100 + entry_SYSCALL_64_after_hwframe+0x76/0x7e + + + Allocated by task 13305: + ceph_osdmap_alloc+0x16/0x130 + ceph_osdc_init+0x27a/0x4c0 + ceph_create_client+0x153/0x190 + create_fs_client+0x50/0x2a0 + ceph_get_tree+0xff/0x680 + vfs_get_tree+0x49/0x180 + do_new_mount+0x1a3/0x2d0 + path_mount+0x6dd/0x730 + do_mount+0x99/0xe0 + __do_sys_mount+0x141/0x180 + do_syscall_64+0x9f/0x100 + entry_SYSCALL_64_after_hwframe+0x76/0x7e + + Freed by task 9475: + kfree+0x212/0x290 + handle_one_map+0x23c/0x3b0 + ceph_osdc_handle_map+0x3c9/0x590 + mon_dispatch+0x655/0x6f0 + ceph_con_process_message+0xc3/0xe0 + ceph_con_v1_try_read+0x614/0x760 + ceph_con_workfn+0x2de/0x650 + process_one_work+0x486/0x7c0 + process_scheduled_works+0x73/0x90 + worker_thread+0x1c8/0x2a0 + kthread+0x2ec/0x300 + ret_from_fork+0x24/0x40 + ret_from_fork_asm+0x1a/0x30 + +Rewrite the wait loop to check the above condition directly with +client->monc.mutex and client->osdc.lock taken as appropriate. While +at it, improve the timeout handling (previously mount_timeout could be +exceeded in case wait_event_interruptible_timeout() slept more than +once) and access client->auth_err under client->monc.mutex to match +how it's set in finish_auth(). + +monmap_show() and osdmap_show() now take the respective lock before +accessing the map as well. + +Cc: stable@vger.kernel.org +Reported-by: David Howells +Signed-off-by: Ilya Dryomov +Reviewed-by: Viacheslav Dubeyko +--- + net/ceph/ceph_common.c | 53 +++++++++++++++++++++++++----------------- + net/ceph/debugfs.c | 14 +++++++---- + 2 files changed, 42 insertions(+), 25 deletions(-) + +diff --git a/net/ceph/ceph_common.c b/net/ceph/ceph_common.c +index 4c6441536d55b..285e981730e5c 100644 +--- a/net/ceph/ceph_common.c ++++ b/net/ceph/ceph_common.c +@@ -785,42 +785,53 @@ void ceph_reset_client_addr(struct ceph_client *client) + } + EXPORT_SYMBOL(ceph_reset_client_addr); + +-/* +- * true if we have the mon map (and have thus joined the cluster) +- */ +-static bool have_mon_and_osd_map(struct ceph_client *client) +-{ +- return client->monc.monmap && client->monc.monmap->epoch && +- client->osdc.osdmap && client->osdc.osdmap->epoch; +-} +- + /* + * mount: join the ceph cluster, and open root directory. + */ + int __ceph_open_session(struct ceph_client *client, unsigned long started) + { +- unsigned long timeout = client->options->mount_timeout; +- long err; ++ DEFINE_WAIT_FUNC(wait, woken_wake_function); ++ long timeout = ceph_timeout_jiffies(client->options->mount_timeout); ++ bool have_monmap, have_osdmap; ++ int err; + + /* open session, and wait for mon and osd maps */ + err = ceph_monc_open_session(&client->monc); + if (err < 0) + return err; + +- while (!have_mon_and_osd_map(client)) { +- if (timeout && time_after_eq(jiffies, started + timeout)) +- return -ETIMEDOUT; ++ add_wait_queue(&client->auth_wq, &wait); ++ for (;;) { ++ mutex_lock(&client->monc.mutex); ++ err = client->auth_err; ++ have_monmap = client->monc.monmap && client->monc.monmap->epoch; ++ mutex_unlock(&client->monc.mutex); ++ ++ down_read(&client->osdc.lock); ++ have_osdmap = client->osdc.osdmap && client->osdc.osdmap->epoch; ++ up_read(&client->osdc.lock); ++ ++ if (err || (have_monmap && have_osdmap)) ++ break; ++ ++ if (signal_pending(current)) { ++ err = -ERESTARTSYS; ++ break; ++ } ++ ++ if (!timeout) { ++ err = -ETIMEDOUT; ++ break; ++ } + + /* wait */ + dout("mount waiting for mon_map\n"); +- err = wait_event_interruptible_timeout(client->auth_wq, +- have_mon_and_osd_map(client) || (client->auth_err < 0), +- ceph_timeout_jiffies(timeout)); +- if (err < 0) +- return err; +- if (client->auth_err < 0) +- return client->auth_err; ++ timeout = wait_woken(&wait, TASK_INTERRUPTIBLE, timeout); + } ++ remove_wait_queue(&client->auth_wq, &wait); ++ ++ if (err) ++ return err; + + pr_info("client%llu fsid %pU\n", ceph_client_gid(client), + &client->fsid); +diff --git a/net/ceph/debugfs.c b/net/ceph/debugfs.c +index 2110439f8a247..83c270bce63c1 100644 +--- a/net/ceph/debugfs.c ++++ b/net/ceph/debugfs.c +@@ -36,8 +36,9 @@ static int monmap_show(struct seq_file *s, void *p) + int i; + struct ceph_client *client = s->private; + ++ mutex_lock(&client->monc.mutex); + if (client->monc.monmap == NULL) +- return 0; ++ goto out_unlock; + + seq_printf(s, "epoch %d\n", client->monc.monmap->epoch); + for (i = 0; i < client->monc.monmap->num_mon; i++) { +@@ -48,6 +49,9 @@ static int monmap_show(struct seq_file *s, void *p) + ENTITY_NAME(inst->name), + ceph_pr_addr(&inst->addr)); + } ++ ++out_unlock: ++ mutex_unlock(&client->monc.mutex); + return 0; + } + +@@ -56,13 +60,14 @@ static int osdmap_show(struct seq_file *s, void *p) + int i; + struct ceph_client *client = s->private; + struct ceph_osd_client *osdc = &client->osdc; +- struct ceph_osdmap *map = osdc->osdmap; ++ struct ceph_osdmap *map; + struct rb_node *n; + ++ down_read(&osdc->lock); ++ map = osdc->osdmap; + if (map == NULL) +- return 0; ++ goto out_unlock; + +- down_read(&osdc->lock); + seq_printf(s, "epoch %u barrier %u flags 0x%x\n", map->epoch, + osdc->epoch_barrier, map->flags); + +@@ -131,6 +136,7 @@ static int osdmap_show(struct seq_file *s, void *p) + seq_printf(s, "]\n"); + } + ++out_unlock: + up_read(&osdc->lock); + return 0; + } +-- +2.53.0 + +EOF + +cat << 'EOF' > expected +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- b/net/ceph/ceph_common.c ++++ b/net/ceph/ceph_common.c +@@ -678,5 +678,5 @@ + } +-EXPORT_SYMBOL(ceph_destroy_client); ++EXPORT_SYMBOL(ceph_reset_client_addr); + + /* + * true if we have the mon map (and have thus joined the cluster) +--- b/net/ceph/debugfs.c ++++ b/net/ceph/debugfs.c +@@ -45,5 +46,5 @@ + ENTITY_NAME(inst->name), +- ceph_pr_addr(&inst->addr.in_addr)); ++ ceph_pr_addr(&inst->addr)); + } + return 0; + } +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy11/run-test b/tests/fuzzy11/run-test new file mode 100755 index 00000000..3de9a61d --- /dev/null +++ b/tests/fuzzy11/run-test @@ -0,0 +1,29 @@ +#!/bin/sh +# Test '\ No newline at end of file' marker handling in fuzzy mode +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +diff --git a/net/smc/Kconfig b/net/smc/Kconfig +index 325addf83cc69..277ef504bc26e 100644 +--- a/net/smc/Kconfig ++++ b/net/smc/Kconfig +@@ -22,10 +22,10 @@ config SMC_DIAG + + config SMC_HS_CTRL_BPF + bool "Generic eBPF hook for SMC handshake flow" +- depends on SMC && BPF_SYSCALL ++ depends on SMC && BPF_JIT && BPF_SYSCALL + default y + help + SMC_HS_CTRL_BPF enables support to register generic eBPF hook for SMC + handshake flow, which offer much greater flexibility in modifying the behavior + of the SMC protocol stack compared to a complete kernel-based approach. Select +- this option if you want filtring the handshake process via eBPF programs. +\ No newline at end of file ++ this option if you want filtring the handshake process via eBPF programs. +EOF + +${INTERDIFF} --fuzzy patch1 patch1 2>errors >output +[ -s errors ] && exit 1 +[ -s output ] && exit 1 +exit 0 diff --git a/tests/fuzzy12/run-test b/tests/fuzzy12/run-test new file mode 100755 index 00000000..d3f82362 --- /dev/null +++ b/tests/fuzzy12/run-test @@ -0,0 +1,20 @@ +#!/bin/sh +# Test addition at line 1 with zero pre-context in fuzzy mode +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +diff --git a/net/mac80211/mlme.c b/net/mac80211/mlme.c +index 26f15d6bf71be..8d2514a9a6c46 100644 +--- a/net/mac80211/mlme.c ++++ b/net/mac80211/mlme.c +@@ -1,3 +1,4 @@ ++// SPDX-License-Identifier: GPL-2.0-only + /* + * BSS client mode implementation + * Copyright 2003-2008, Jouni Malinen +EOF + +${INTERDIFF} --fuzzy patch1 patch1 2>errors >output +[ -s errors ] && exit 1 +[ -s output ] && exit 1 +exit 0 diff --git a/tests/fuzzy13/run-test b/tests/fuzzy13/run-test new file mode 100755 index 00000000..6a5d6159 --- /dev/null +++ b/tests/fuzzy13/run-test @@ -0,0 +1,40 @@ +#!/bin/sh +# Test file deletion (--- a/file, +++ /dev/null) handling in fuzzy mode +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +diff --git a/drivers/genpd/imx/Makefile b/drivers/genpd/imx/Makefile +new file mode 100644 +index 0000000000000..5f012717a6663 +--- /dev/null ++++ b/drivers/genpd/imx/Makefile +@@ -0,0 +1,7 @@ ++# SPDX-License-Identifier: GPL-2.0-only ++obj-$(CONFIG_HAVE_IMX_GPC) += gpc.o ++obj-$(CONFIG_IMX_GPCV2_PM_DOMAINS) += gpcv2.o ++obj-$(CONFIG_IMX8M_BLK_CTRL) += imx8m-blk-ctrl.o ++obj-$(CONFIG_IMX8M_BLK_CTRL) += imx8mp-blk-ctrl.o ++obj-$(CONFIG_SOC_IMX9) += imx93-pd.o ++obj-$(CONFIG_IMX9_BLK_CTRL) += imx93-blk-ctrl.o +diff --git a/drivers/soc/imx/Makefile b/drivers/soc/imx/Makefile +index a28c44a1f16af..3ad321ca608a6 100644 +--- a/drivers/soc/imx/Makefile ++++ b/drivers/soc/imx/Makefile +@@ -2,10 +2,5 @@ + ifeq ($(CONFIG_ARM),y) + obj-$(CONFIG_ARCH_MXC) += soc-imx.o + endif +-obj-$(CONFIG_HAVE_IMX_GPC) += gpc.o +-obj-$(CONFIG_IMX_GPCV2_PM_DOMAINS) += gpcv2.o + obj-$(CONFIG_SOC_IMX8M) += soc-imx8m.o +-obj-$(CONFIG_IMX8M_BLK_CTRL) += imx8m-blk-ctrl.o +-obj-$(CONFIG_IMX8M_BLK_CTRL) += imx8mp-blk-ctrl.o +-obj-$(CONFIG_SOC_IMX9) += imx93-src.o imx93-pd.o +-obj-$(CONFIG_IMX9_BLK_CTRL) += imx93-blk-ctrl.o ++obj-$(CONFIG_SOC_IMX9) += imx93-src.o +EOF + +${INTERDIFF} --fuzzy patch1 patch1 2>errors >output +[ -s errors ] && exit 1 +[ -s output ] && exit 1 +exit 0 diff --git a/tests/fuzzy14/run-test b/tests/fuzzy14/run-test new file mode 100755 index 00000000..0a07c1cf --- /dev/null +++ b/tests/fuzzy14/run-test @@ -0,0 +1,35 @@ +#!/bin/sh +# Test mode-only patches with no diff hunks in fuzzy mode +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +diff --git a/tools/testing/selftests/mm/charge_reserved_hugetlb.sh b/tools/testing/selftests/mm/charge_reserved_hugetlb.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/check_config.sh b/tools/testing/selftests/mm/check_config.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/hugetlb_reparenting_test.sh b/tools/testing/selftests/mm/hugetlb_reparenting_test.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/test_hmm.sh b/tools/testing/selftests/mm/test_hmm.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/test_vmalloc.sh b/tools/testing/selftests/mm/test_vmalloc.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/va_high_addr_switch.sh b/tools/testing/selftests/mm/va_high_addr_switch.sh +old mode 100644 +new mode 100755 +diff --git a/tools/testing/selftests/mm/write_hugetlb_memory.sh b/tools/testing/selftests/mm/write_hugetlb_memory.sh +old mode 100644 +new mode 100755 +EOF + +${INTERDIFF} --fuzzy patch1 patch1 2>errors >output +[ -s errors ] && exit 1 +[ -s output ] && exit 1 +exit 0 diff --git a/tests/fuzzy2/run-test b/tests/fuzzy2/run-test new file mode 100755 index 00000000..8d41cd89 --- /dev/null +++ b/tests/fuzzy2/run-test @@ -0,0 +1,42 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with line offsets successfully fuzzed. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +EOF + +cat << 'EOF' > patch2 +--- file ++++ file +@@ -50,9 +50,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 +[ -s output ] && exit 1 +exit 0 diff --git a/tests/fuzzy3/run-test b/tests/fuzzy3/run-test new file mode 100755 index 00000000..8bac4355 --- /dev/null +++ b/tests/fuzzy3/run-test @@ -0,0 +1,55 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with differing context lines and line offsets fuzzed. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +EOF + +cat << 'EOF' > patch2 +--- file ++++ file +@@ -50,9 +50,6 @@ + line 6 + if + 1 +-fi +-if +-2 + fi + B + C +EOF + +cat << 'EOF' > expected +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- file ++++ file +@@ -7,3 +8,2 @@ + fi +-A + B +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy4/run-test b/tests/fuzzy4/run-test new file mode 100755 index 00000000..755a721a --- /dev/null +++ b/tests/fuzzy4/run-test @@ -0,0 +1,89 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing test of the optional N argument to --fuzzy. Triggers +# rejects by setting the fuzz value to 1, when it could've been fuzzed with the +# default fuzz value of 2 in `patch`. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +EOF + +cat << 'EOF' > patch2 +--- file ++++ file +@@ -50,9 +50,6 @@ + line 6 + if + 1 +-fi +-if +-2 + fi + B + C +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- file ++++ file +@@ -7,4 +7,7 @@ + 1 + fi ++if ++2 ++fi + A + B + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- file ++++ file +@@ -50,9 +50,6 @@ + line 6 + if + 1 +-fi +-if +-2 + fi + B + C + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- file ++++ file +@@ -7,3 +8,2 @@ + fi +-A + B +EOF + +${INTERDIFF} --fuzzy=1 patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy5/run-test b/tests/fuzzy5/run-test new file mode 100755 index 00000000..e1dd67d6 --- /dev/null +++ b/tests/fuzzy5/run-test @@ -0,0 +1,126 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with more than one rejected hunk per patched file. This +# also tests the hunk parser by inserting whitespace between the +++ line and +# the first hunk, which should be gracefully ignored. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file + + + +@@ -5,9 +5,6 @@ + line 5 + if + 1 +-fi +-if +-2 + fi + A + B +@@ -7,7 +7,7 @@ + C + 9 + 8 +-1 ++7 + D + E + if +EOF + +cat << 'EOF' > patch2 +--- file ++++ file + +@@ -50,9 +50,6 @@ + line 6 + if + 1 +-fi +-if +-2 + fi + B + C +@@ -7,7 +7,7 @@ + D + Z + 1 +-1 ++7 + F + E + if +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- file ++++ file +@@ -6,8 +6,8 @@ + if + 1 + fi ++if ++2 ++fi + A + B +-D +-E +-if + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- file ++++ file +@@ -50,9 +50,6 @@ + line 6 + if + 1 +-fi +-if +-2 + fi + B + C +@@ -7,7 +7,7 @@ + D + Z + 1 +-1 ++7 + F + E + if + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- file ++++ file +@@ -2 +2,0 @@ +-line 5 +@@ -8,3 +18,2 @@ + fi +-A + B +EOF + +${INTERDIFF} --fuzzy=1 patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy6/run-test b/tests/fuzzy6/run-test new file mode 100755 index 00000000..7a76d75b --- /dev/null +++ b/tests/fuzzy6/run-test @@ -0,0 +1,55 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with the hunk splitter stressed by having a +/- line as +# either the first or last line in a hunk, while triggering a split by +# separating two deltas by some context lines. This also tests patches with an +# unequal number of pre-context and post-context lines. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +--- file ++++ file +@@ -5,2 +5,2 @@ +-line 5 + if ++1 +EOF + +cat << 'EOF' > patch2 +--- file ++++ file +@@ -50,5 +50,5 @@ + hi + line 4 +-line 5 + if ++1 + 2 +EOF + +cat << 'EOF' > expected +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- file ++++ file +@@ -50,5 +50,4 @@ + hi + line 4 +-line 5 + if + 2 +@@ -53,2 +52,3 @@ + if ++1 + 2 +EOF + +${INTERDIFF} --fuzzy=1 patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy7/run-test b/tests/fuzzy7/run-test new file mode 100755 index 00000000..7a93d827 --- /dev/null +++ b/tests/fuzzy7/run-test @@ -0,0 +1,918 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing with a real Linux kernel backport compared against its +# upstream version. Stresses having multiple relocations and most of the fuzzy +# diffing machinery as a whole. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +From ed0708f971a26619964eea4e9eccb73847caa0c7 Mon Sep 17 00:00:00 2001 +From: Shreeya Patel +Date: Wed, 3 Sep 2025 12:36:41 +0000 +Subject: [PATCH] net: mana: Handle Reset Request from MANA NIC + +jira LE-3923 +commit-author Haiyang Zhang +commit fbe346ce9d626680a4dd0f079e17c7b5dd32ffad +upstream-diff There were conflicts seen when applying this +patch due to the following missing commits :- +ca8ac489ca33 ("net: mana: Handle unsupported HWC commands") +505cc26bcae0 ("net: mana: Add support for auxiliary device servicing +events") + +Upon receiving the Reset Request, pause the connection and clean up +queues, wait for the specified period, then resume the NIC. +In the cleanup phase, the HWC is no longer responding, so set hwc_timeout +to zero to skip waiting on the response. + + Signed-off-by: Haiyang Zhang +Link: https://patch.msgid.link/1751055983-29760-1-git-send-email-haiyangz@linux.microsoft.com + Signed-off-by: Jakub Kicinski +(cherry picked from commit fbe346ce9d626680a4dd0f079e17c7b5dd32ffad) + Signed-off-by: Shreeya Patel +--- + .../net/ethernet/microsoft/mana/gdma_main.c | 127 ++++++++++++++---- + .../net/ethernet/microsoft/mana/hw_channel.c | 4 +- + drivers/net/ethernet/microsoft/mana/mana_en.c | 37 +++-- + include/net/mana/gdma.h | 10 ++ + 4 files changed, 143 insertions(+), 35 deletions(-) + +diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c b/drivers/net/ethernet/microsoft/mana/gdma_main.c +index 9d19df7cd82b3..cf3b920476cf6 100644 +--- a/drivers/net/ethernet/microsoft/mana/gdma_main.c ++++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c +@@ -8,6 +8,7 @@ + #include + + #include ++#include + + #include + struct dentry *mana_debugfs_root; +@@ -64,6 +65,24 @@ static void mana_gd_init_registers(struct pci_dev *pdev) + mana_gd_init_vf_regs(pdev); + } + ++/* Suppress logging when we set timeout to zeo */ ++bool mana_need_log(struct gdma_context *gc, int err) ++{ ++ struct hw_channel_context *hwc; ++ ++ if (err != -ETIMEDOUT) ++ return true; ++ ++ if (!gc) ++ return true; ++ ++ hwc = gc->hwc.driver_data; ++ if (hwc && hwc->hwc_timeout == 0) ++ return false; ++ ++ return true; ++} ++ + static int mana_gd_query_max_resources(struct pci_dev *pdev) + { + struct gdma_context *gc = pci_get_drvdata(pdev); +@@ -267,8 +286,9 @@ static int mana_gd_disable_queue(struct gdma_queue *queue) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to disable queue: %d, 0x%x\n", err, +- resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to disable queue: %d, 0x%x\n", err, ++ resp.hdr.status); + return err ? err : -EPROTO; + } + +@@ -353,25 +373,12 @@ void mana_gd_ring_cq(struct gdma_queue *cq, u8 arm_bit) + + #define MANA_SERVICE_PERIOD 10 + +-struct mana_serv_work { +- struct work_struct serv_work; +- struct pci_dev *pdev; +-}; +- +-static void mana_serv_func(struct work_struct *w) ++static void mana_serv_fpga(struct pci_dev *pdev) + { +- struct mana_serv_work *mns_wk; + struct pci_bus *bus, *parent; +- struct pci_dev *pdev; +- +- mns_wk = container_of(w, struct mana_serv_work, serv_work); +- pdev = mns_wk->pdev; + + pci_lock_rescan_remove(); + +- if (!pdev) +- goto out; +- + bus = pdev->bus; + if (!bus) { + dev_err(&pdev->dev, "MANA service: no bus\n"); +@@ -392,7 +399,74 @@ static void mana_serv_func(struct work_struct *w) + + out: + pci_unlock_rescan_remove(); ++} ++ ++static void mana_serv_reset(struct pci_dev *pdev) ++{ ++ struct gdma_context *gc = pci_get_drvdata(pdev); ++ struct hw_channel_context *hwc; ++ ++ if (!gc) { ++ dev_err(&pdev->dev, "MANA service: no GC\n"); ++ return; ++ } ++ ++ hwc = gc->hwc.driver_data; ++ if (!hwc) { ++ dev_err(&pdev->dev, "MANA service: no HWC\n"); ++ goto out; ++ } ++ ++ /* HWC is not responding in this case, so don't wait */ ++ hwc->hwc_timeout = 0; ++ ++ dev_info(&pdev->dev, "MANA reset cycle start\n"); + ++ mana_gd_suspend(pdev, PMSG_SUSPEND); ++ ++ msleep(MANA_SERVICE_PERIOD * 1000); ++ ++ mana_gd_resume(pdev); ++ ++ dev_info(&pdev->dev, "MANA reset cycle completed\n"); ++ ++out: ++ gc->in_service = false; ++} ++ ++struct mana_serv_work { ++ struct work_struct serv_work; ++ struct pci_dev *pdev; ++ enum gdma_eqe_type type; ++}; ++ ++static void mana_serv_func(struct work_struct *w) ++{ ++ struct mana_serv_work *mns_wk; ++ struct pci_dev *pdev; ++ ++ mns_wk = container_of(w, struct mana_serv_work, serv_work); ++ pdev = mns_wk->pdev; ++ ++ if (!pdev) ++ goto out; ++ ++ switch (mns_wk->type) { ++ case GDMA_EQE_HWC_FPGA_RECONFIG: ++ mana_serv_fpga(pdev); ++ break; ++ ++ case GDMA_EQE_HWC_RESET_REQUEST: ++ mana_serv_reset(pdev); ++ break; ++ ++ default: ++ dev_err(&pdev->dev, "MANA service: unknown type %d\n", ++ mns_wk->type); ++ break; ++ } ++ ++out: + pci_dev_put(pdev); + kfree(mns_wk); + module_put(THIS_MODULE); +@@ -448,6 +522,7 @@ static void mana_gd_process_eqe(struct gdma_queue *eq) + break; + + case GDMA_EQE_HWC_FPGA_RECONFIG: ++ case GDMA_EQE_HWC_RESET_REQUEST: + dev_info(gc->dev, "Recv MANA service type:%d\n", type); + + if (gc->in_service) { +@@ -469,6 +544,7 @@ static void mana_gd_process_eqe(struct gdma_queue *eq) + dev_info(gc->dev, "Start MANA service type:%d\n", type); + gc->in_service = true; + mns_wk->pdev = to_pci_dev(gc->dev); ++ mns_wk->type = type; + pci_dev_get(mns_wk->pdev); + INIT_WORK(&mns_wk->serv_work, mana_serv_func); + schedule_work(&mns_wk->serv_work); +@@ -615,7 +691,8 @@ int mana_gd_test_eq(struct gdma_context *gc, struct gdma_queue *eq) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err) { +- dev_err(dev, "test_eq failed: %d\n", err); ++ if (mana_need_log(gc, err)) ++ dev_err(dev, "test_eq failed: %d\n", err); + goto out; + } + +@@ -650,7 +727,7 @@ static void mana_gd_destroy_eq(struct gdma_context *gc, bool flush_evenets, + + if (flush_evenets) { + err = mana_gd_test_eq(gc, queue); +- if (err) ++ if (err && mana_need_log(gc, err)) + dev_warn(gc->dev, "Failed to flush EQ: %d\n", err); + } + +@@ -796,8 +873,9 @@ int mana_gd_destroy_dma_region(struct gdma_context *gc, u64 dma_region_handle) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to destroy DMA region: %d, 0x%x\n", +- err, resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to destroy DMA region: %d, 0x%x\n", ++ err, resp.hdr.status); + return -EPROTO; + } + +@@ -1096,8 +1174,9 @@ int mana_gd_deregister_device(struct gdma_dev *gd) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to deregister device: %d, 0x%x\n", +- err, resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to deregister device: %d, 0x%x\n", ++ err, resp.hdr.status); + if (!err) + err = -EPROTO; + } +@@ -1697,7 +1776,7 @@ static void mana_gd_remove(struct pci_dev *pdev) + } + + /* The 'state' parameter is not used. */ +-static int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) ++int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) + { + struct gdma_context *gc = pci_get_drvdata(pdev); + +@@ -1712,7 +1791,7 @@ static int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) + * fail -- if this happens, it's safer to just report an error than try to undo + * what has been done. + */ +-static int mana_gd_resume(struct pci_dev *pdev) ++int mana_gd_resume(struct pci_dev *pdev) + { + struct gdma_context *gc = pci_get_drvdata(pdev); + int err; +diff --git a/drivers/net/ethernet/microsoft/mana/hw_channel.c b/drivers/net/ethernet/microsoft/mana/hw_channel.c +index 4291a2fc2710e..aed60be5ee389 100644 +--- a/drivers/net/ethernet/microsoft/mana/hw_channel.c ++++ b/drivers/net/ethernet/microsoft/mana/hw_channel.c +@@ -854,7 +854,9 @@ int mana_hwc_send_request(struct hw_channel_context *hwc, u32 req_len, + + if (!wait_for_completion_timeout(&ctx->comp_event, + (msecs_to_jiffies(hwc->hwc_timeout)))) { +- dev_err(hwc->dev, "HWC: Request timed out!\n"); ++ if (hwc->hwc_timeout != 0) ++ dev_err(hwc->dev, "HWC: Request timed out!\n"); ++ + err = -ETIMEDOUT; + goto out; + } +diff --git a/drivers/net/ethernet/microsoft/mana/mana_en.c b/drivers/net/ethernet/microsoft/mana/mana_en.c +index cbecacf503422..acf1342536463 100644 +--- a/drivers/net/ethernet/microsoft/mana/mana_en.c ++++ b/drivers/net/ethernet/microsoft/mana/mana_en.c +@@ -45,6 +45,15 @@ static const struct file_operations mana_dbg_q_fops = { + .read = mana_dbg_q_read, + }; + ++static bool mana_en_need_log(struct mana_port_context *apc, int err) ++{ ++ if (apc && apc->ac && apc->ac->gdma_dev && ++ apc->ac->gdma_dev->gdma_context) ++ return mana_need_log(apc->ac->gdma_dev->gdma_context, err); ++ else ++ return true; ++} ++ + /* Microsoft Azure Network Adapter (MANA) functions */ + + static int mana_open(struct net_device *ndev) +@@ -768,7 +777,8 @@ static int mana_send_request(struct mana_context *ac, void *in_buf, + err = mana_gd_send_request(gc, in_len, in_buf, out_len, + out_buf); + if (err || resp->status) { +- if (req->req.msg_type != MANA_QUERY_PHY_STAT) ++ if (req->req.msg_type != MANA_QUERY_PHY_STAT && ++ mana_need_log(gc, err)) + dev_err(dev, "Failed to send mana message: %d, 0x%x\n", + err, resp->status); + return err ? err : -EPROTO; +@@ -845,8 +855,10 @@ static void mana_pf_deregister_hw_vport(struct mana_port_context *apc) + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(apc->ndev, "Failed to unregister hw vPort: %d\n", +- err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(apc->ndev, "Failed to unregister hw vPort: %d\n", ++ err); ++ + return; + } + +@@ -901,8 +913,10 @@ static void mana_pf_deregister_filter(struct mana_port_context *apc) + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(apc->ndev, "Failed to unregister filter: %d\n", +- err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(apc->ndev, "Failed to unregister filter: %d\n", ++ err); ++ + return; + } + +@@ -1132,7 +1146,9 @@ static int mana_cfg_vport_steering(struct mana_port_context *apc, + err = mana_send_request(apc->ac, req, req_buf_size, &resp, + sizeof(resp)); + if (err) { +- netdev_err(ndev, "Failed to configure vPort RX: %d\n", err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(ndev, "Failed to configure vPort RX: %d\n", err); ++ + goto out; + } + +@@ -1227,7 +1243,9 @@ void mana_destroy_wq_obj(struct mana_port_context *apc, u32 wq_type, + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(ndev, "Failed to destroy WQ object: %d\n", err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(ndev, "Failed to destroy WQ object: %d\n", err); ++ + return; + } + +@@ -2872,11 +2890,10 @@ static int mana_dealloc_queues(struct net_device *ndev) + + apc->rss_state = TRI_STATE_FALSE; + err = mana_config_rss(apc, TRI_STATE_FALSE, false, false); +- if (err) { ++ if (err && mana_en_need_log(apc, err)) + netdev_err(ndev, "Failed to disable vPort: %d\n", err); +- return err; +- } + ++ /* Even in err case, still need to cleanup the vPort */ + mana_destroy_vport(apc); + + return 0; +diff --git a/include/net/mana/gdma.h b/include/net/mana/gdma.h +index b602b2e55939c..af5596bf46878 100644 +--- a/include/net/mana/gdma.h ++++ b/include/net/mana/gdma.h +@@ -60,6 +60,7 @@ enum gdma_eqe_type { + GDMA_EQE_HWC_INIT_DONE = 131, + GDMA_EQE_HWC_FPGA_RECONFIG = 132, + GDMA_EQE_HWC_SOC_RECONFIG_DATA = 133, ++ GDMA_EQE_HWC_RESET_REQUEST = 135, + GDMA_EQE_RNIC_QP_FATAL = 176, + }; + +@@ -559,6 +560,9 @@ enum { + /* Driver can handle holes (zeros) in the device list */ + #define GDMA_DRV_CAP_FLAG_1_DEV_LIST_HOLES_SUP BIT(11) + ++/* Driver can self reset on EQE notification */ ++#define GDMA_DRV_CAP_FLAG_1_SELF_RESET_ON_EQE BIT(14) ++ + /* Driver can self reset on FPGA Reconfig EQE notification */ + #define GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE BIT(17) + +@@ -568,6 +572,7 @@ enum { + GDMA_DRV_CAP_FLAG_1_HWC_TIMEOUT_RECONFIG | \ + GDMA_DRV_CAP_FLAG_1_VARIABLE_INDIRECTION_TABLE_SUPPORT | \ + GDMA_DRV_CAP_FLAG_1_DEV_LIST_HOLES_SUP | \ ++ GDMA_DRV_CAP_FLAG_1_SELF_RESET_ON_EQE | \ + GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE) + + #define GDMA_DRV_CAP_FLAGS2 0 +@@ -892,4 +897,9 @@ int mana_gd_destroy_dma_region(struct gdma_context *gc, u64 dma_region_handle); + void mana_register_debugfs(void); + void mana_unregister_debugfs(void); + ++int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state); ++int mana_gd_resume(struct pci_dev *pdev); ++ ++bool mana_need_log(struct gdma_context *gc, int err); ++ + #endif /* _GDMA_H */ +-- +2.51.2 + +EOF + +cat << 'EOF' > patch2 +From fbe346ce9d626680a4dd0f079e17c7b5dd32ffad Mon Sep 17 00:00:00 2001 +From: Haiyang Zhang +Date: Fri, 27 Jun 2025 13:26:23 -0700 +Subject: [PATCH] net: mana: Handle Reset Request from MANA NIC + +Upon receiving the Reset Request, pause the connection and clean up +queues, wait for the specified period, then resume the NIC. +In the cleanup phase, the HWC is no longer responding, so set hwc_timeout +to zero to skip waiting on the response. + +Signed-off-by: Haiyang Zhang +Link: https://patch.msgid.link/1751055983-29760-1-git-send-email-haiyangz@linux.microsoft.com +Signed-off-by: Jakub Kicinski +--- + .../net/ethernet/microsoft/mana/gdma_main.c | 127 ++++++++++++++---- + .../net/ethernet/microsoft/mana/hw_channel.c | 4 +- + drivers/net/ethernet/microsoft/mana/mana_en.c | 37 +++-- + include/net/mana/gdma.h | 10 ++ + 4 files changed, 143 insertions(+), 35 deletions(-) + +diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c b/drivers/net/ethernet/microsoft/mana/gdma_main.c +index 55dd7dee718cc..a468cd8e5f361 100644 +--- a/drivers/net/ethernet/microsoft/mana/gdma_main.c ++++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c +@@ -10,6 +10,7 @@ + #include + + #include ++#include + + struct dentry *mana_debugfs_root; + +@@ -68,6 +69,24 @@ static void mana_gd_init_registers(struct pci_dev *pdev) + mana_gd_init_vf_regs(pdev); + } + ++/* Suppress logging when we set timeout to zero */ ++bool mana_need_log(struct gdma_context *gc, int err) ++{ ++ struct hw_channel_context *hwc; ++ ++ if (err != -ETIMEDOUT) ++ return true; ++ ++ if (!gc) ++ return true; ++ ++ hwc = gc->hwc.driver_data; ++ if (hwc && hwc->hwc_timeout == 0) ++ return false; ++ ++ return true; ++} ++ + static int mana_gd_query_max_resources(struct pci_dev *pdev) + { + struct gdma_context *gc = pci_get_drvdata(pdev); +@@ -278,8 +297,9 @@ static int mana_gd_disable_queue(struct gdma_queue *queue) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to disable queue: %d, 0x%x\n", err, +- resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to disable queue: %d, 0x%x\n", err, ++ resp.hdr.status); + return err ? err : -EPROTO; + } + +@@ -366,25 +386,12 @@ EXPORT_SYMBOL_NS(mana_gd_ring_cq, "NET_MANA"); + + #define MANA_SERVICE_PERIOD 10 + +-struct mana_serv_work { +- struct work_struct serv_work; +- struct pci_dev *pdev; +-}; +- +-static void mana_serv_func(struct work_struct *w) ++static void mana_serv_fpga(struct pci_dev *pdev) + { +- struct mana_serv_work *mns_wk; + struct pci_bus *bus, *parent; +- struct pci_dev *pdev; +- +- mns_wk = container_of(w, struct mana_serv_work, serv_work); +- pdev = mns_wk->pdev; + + pci_lock_rescan_remove(); + +- if (!pdev) +- goto out; +- + bus = pdev->bus; + if (!bus) { + dev_err(&pdev->dev, "MANA service: no bus\n"); +@@ -405,7 +412,74 @@ static void mana_serv_func(struct work_struct *w) + + out: + pci_unlock_rescan_remove(); ++} ++ ++static void mana_serv_reset(struct pci_dev *pdev) ++{ ++ struct gdma_context *gc = pci_get_drvdata(pdev); ++ struct hw_channel_context *hwc; ++ ++ if (!gc) { ++ dev_err(&pdev->dev, "MANA service: no GC\n"); ++ return; ++ } ++ ++ hwc = gc->hwc.driver_data; ++ if (!hwc) { ++ dev_err(&pdev->dev, "MANA service: no HWC\n"); ++ goto out; ++ } ++ ++ /* HWC is not responding in this case, so don't wait */ ++ hwc->hwc_timeout = 0; ++ ++ dev_info(&pdev->dev, "MANA reset cycle start\n"); + ++ mana_gd_suspend(pdev, PMSG_SUSPEND); ++ ++ msleep(MANA_SERVICE_PERIOD * 1000); ++ ++ mana_gd_resume(pdev); ++ ++ dev_info(&pdev->dev, "MANA reset cycle completed\n"); ++ ++out: ++ gc->in_service = false; ++} ++ ++struct mana_serv_work { ++ struct work_struct serv_work; ++ struct pci_dev *pdev; ++ enum gdma_eqe_type type; ++}; ++ ++static void mana_serv_func(struct work_struct *w) ++{ ++ struct mana_serv_work *mns_wk; ++ struct pci_dev *pdev; ++ ++ mns_wk = container_of(w, struct mana_serv_work, serv_work); ++ pdev = mns_wk->pdev; ++ ++ if (!pdev) ++ goto out; ++ ++ switch (mns_wk->type) { ++ case GDMA_EQE_HWC_FPGA_RECONFIG: ++ mana_serv_fpga(pdev); ++ break; ++ ++ case GDMA_EQE_HWC_RESET_REQUEST: ++ mana_serv_reset(pdev); ++ break; ++ ++ default: ++ dev_err(&pdev->dev, "MANA service: unknown type %d\n", ++ mns_wk->type); ++ break; ++ } ++ ++out: + pci_dev_put(pdev); + kfree(mns_wk); + module_put(THIS_MODULE); +@@ -462,6 +536,7 @@ static void mana_gd_process_eqe(struct gdma_queue *eq) + break; + + case GDMA_EQE_HWC_FPGA_RECONFIG: ++ case GDMA_EQE_HWC_RESET_REQUEST: + dev_info(gc->dev, "Recv MANA service type:%d\n", type); + + if (gc->in_service) { +@@ -483,6 +558,7 @@ static void mana_gd_process_eqe(struct gdma_queue *eq) + dev_info(gc->dev, "Start MANA service type:%d\n", type); + gc->in_service = true; + mns_wk->pdev = to_pci_dev(gc->dev); ++ mns_wk->type = type; + pci_dev_get(mns_wk->pdev); + INIT_WORK(&mns_wk->serv_work, mana_serv_func); + schedule_work(&mns_wk->serv_work); +@@ -634,7 +710,8 @@ int mana_gd_test_eq(struct gdma_context *gc, struct gdma_queue *eq) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err) { +- dev_err(dev, "test_eq failed: %d\n", err); ++ if (mana_need_log(gc, err)) ++ dev_err(dev, "test_eq failed: %d\n", err); + goto out; + } + +@@ -669,7 +746,7 @@ static void mana_gd_destroy_eq(struct gdma_context *gc, bool flush_evenets, + + if (flush_evenets) { + err = mana_gd_test_eq(gc, queue); +- if (err) ++ if (err && mana_need_log(gc, err)) + dev_warn(gc->dev, "Failed to flush EQ: %d\n", err); + } + +@@ -815,8 +892,9 @@ int mana_gd_destroy_dma_region(struct gdma_context *gc, u64 dma_region_handle) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to destroy DMA region: %d, 0x%x\n", +- err, resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to destroy DMA region: %d, 0x%x\n", ++ err, resp.hdr.status); + return -EPROTO; + } + +@@ -1116,8 +1194,9 @@ int mana_gd_deregister_device(struct gdma_dev *gd) + + err = mana_gd_send_request(gc, sizeof(req), &req, sizeof(resp), &resp); + if (err || resp.hdr.status) { +- dev_err(gc->dev, "Failed to deregister device: %d, 0x%x\n", +- err, resp.hdr.status); ++ if (mana_need_log(gc, err)) ++ dev_err(gc->dev, "Failed to deregister device: %d, 0x%x\n", ++ err, resp.hdr.status); + if (!err) + err = -EPROTO; + } +@@ -1915,7 +1994,7 @@ static void mana_gd_remove(struct pci_dev *pdev) + } + + /* The 'state' parameter is not used. */ +-static int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) ++int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) + { + struct gdma_context *gc = pci_get_drvdata(pdev); + +@@ -1931,7 +2010,7 @@ static int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state) + * fail -- if this happens, it's safer to just report an error than try to undo + * what has been done. + */ +-static int mana_gd_resume(struct pci_dev *pdev) ++int mana_gd_resume(struct pci_dev *pdev) + { + struct gdma_context *gc = pci_get_drvdata(pdev); + int err; +diff --git a/drivers/net/ethernet/microsoft/mana/hw_channel.c b/drivers/net/ethernet/microsoft/mana/hw_channel.c +index 650d22654d499..ef072e24c46d0 100644 +--- a/drivers/net/ethernet/microsoft/mana/hw_channel.c ++++ b/drivers/net/ethernet/microsoft/mana/hw_channel.c +@@ -880,7 +880,9 @@ int mana_hwc_send_request(struct hw_channel_context *hwc, u32 req_len, + + if (!wait_for_completion_timeout(&ctx->comp_event, + (msecs_to_jiffies(hwc->hwc_timeout)))) { +- dev_err(hwc->dev, "HWC: Request timed out!\n"); ++ if (hwc->hwc_timeout != 0) ++ dev_err(hwc->dev, "HWC: Request timed out!\n"); ++ + err = -ETIMEDOUT; + goto out; + } +diff --git a/drivers/net/ethernet/microsoft/mana/mana_en.c b/drivers/net/ethernet/microsoft/mana/mana_en.c +index 016fd808ccad4..a7973651ae51b 100644 +--- a/drivers/net/ethernet/microsoft/mana/mana_en.c ++++ b/drivers/net/ethernet/microsoft/mana/mana_en.c +@@ -47,6 +47,15 @@ static const struct file_operations mana_dbg_q_fops = { + .read = mana_dbg_q_read, + }; + ++static bool mana_en_need_log(struct mana_port_context *apc, int err) ++{ ++ if (apc && apc->ac && apc->ac->gdma_dev && ++ apc->ac->gdma_dev->gdma_context) ++ return mana_need_log(apc->ac->gdma_dev->gdma_context, err); ++ else ++ return true; ++} ++ + /* Microsoft Azure Network Adapter (MANA) functions */ + + static int mana_open(struct net_device *ndev) +@@ -854,7 +863,8 @@ static int mana_send_request(struct mana_context *ac, void *in_buf, + if (err == -EOPNOTSUPP) + return err; + +- if (req->req.msg_type != MANA_QUERY_PHY_STAT) ++ if (req->req.msg_type != MANA_QUERY_PHY_STAT && ++ mana_need_log(gc, err)) + dev_err(dev, "Failed to send mana message: %d, 0x%x\n", + err, resp->status); + return err ? err : -EPROTO; +@@ -931,8 +941,10 @@ static void mana_pf_deregister_hw_vport(struct mana_port_context *apc) + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(apc->ndev, "Failed to unregister hw vPort: %d\n", +- err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(apc->ndev, "Failed to unregister hw vPort: %d\n", ++ err); ++ + return; + } + +@@ -987,8 +999,10 @@ static void mana_pf_deregister_filter(struct mana_port_context *apc) + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(apc->ndev, "Failed to unregister filter: %d\n", +- err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(apc->ndev, "Failed to unregister filter: %d\n", ++ err); ++ + return; + } + +@@ -1218,7 +1232,9 @@ static int mana_cfg_vport_steering(struct mana_port_context *apc, + err = mana_send_request(apc->ac, req, req_buf_size, &resp, + sizeof(resp)); + if (err) { +- netdev_err(ndev, "Failed to configure vPort RX: %d\n", err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(ndev, "Failed to configure vPort RX: %d\n", err); ++ + goto out; + } + +@@ -1402,7 +1418,9 @@ void mana_destroy_wq_obj(struct mana_port_context *apc, u32 wq_type, + err = mana_send_request(apc->ac, &req, sizeof(req), &resp, + sizeof(resp)); + if (err) { +- netdev_err(ndev, "Failed to destroy WQ object: %d\n", err); ++ if (mana_en_need_log(apc, err)) ++ netdev_err(ndev, "Failed to destroy WQ object: %d\n", err); ++ + return; + } + +@@ -3067,11 +3085,10 @@ static int mana_dealloc_queues(struct net_device *ndev) + + apc->rss_state = TRI_STATE_FALSE; + err = mana_config_rss(apc, TRI_STATE_FALSE, false, false); +- if (err) { ++ if (err && mana_en_need_log(apc, err)) + netdev_err(ndev, "Failed to disable vPort: %d\n", err); +- return err; +- } + ++ /* Even in err case, still need to cleanup the vPort */ + mana_destroy_vport(apc); + + return 0; +diff --git a/include/net/mana/gdma.h b/include/net/mana/gdma.h +index 92ab85061df00..57df78cfbf82c 100644 +--- a/include/net/mana/gdma.h ++++ b/include/net/mana/gdma.h +@@ -62,6 +62,7 @@ enum gdma_eqe_type { + GDMA_EQE_HWC_FPGA_RECONFIG = 132, + GDMA_EQE_HWC_SOC_RECONFIG_DATA = 133, + GDMA_EQE_HWC_SOC_SERVICE = 134, ++ GDMA_EQE_HWC_RESET_REQUEST = 135, + GDMA_EQE_RNIC_QP_FATAL = 176, + }; + +@@ -584,6 +585,9 @@ enum { + /* Driver supports dynamic MSI-X vector allocation */ + #define GDMA_DRV_CAP_FLAG_1_DYNAMIC_IRQ_ALLOC_SUPPORT BIT(13) + ++/* Driver can self reset on EQE notification */ ++#define GDMA_DRV_CAP_FLAG_1_SELF_RESET_ON_EQE BIT(14) ++ + /* Driver can self reset on FPGA Reconfig EQE notification */ + #define GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE BIT(17) + +@@ -594,6 +598,7 @@ enum { + GDMA_DRV_CAP_FLAG_1_VARIABLE_INDIRECTION_TABLE_SUPPORT | \ + GDMA_DRV_CAP_FLAG_1_DEV_LIST_HOLES_SUP | \ + GDMA_DRV_CAP_FLAG_1_DYNAMIC_IRQ_ALLOC_SUPPORT | \ ++ GDMA_DRV_CAP_FLAG_1_SELF_RESET_ON_EQE | \ + GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE) + + #define GDMA_DRV_CAP_FLAGS2 0 +@@ -921,4 +926,9 @@ void mana_unregister_debugfs(void); + + int mana_rdma_service_event(struct gdma_context *gc, enum gdma_service_type event); + ++int mana_gd_suspend(struct pci_dev *pdev, pm_message_t state); ++int mana_gd_resume(struct pci_dev *pdev); ++ ++bool mana_need_log(struct gdma_context *gc, int err); ++ + #endif /* _GDMA_H */ +-- +2.51.2 + +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- b/drivers/net/ethernet/microsoft/mana/gdma_main.c ++++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c +@@ -65,7 +65,7 @@ + mana_gd_init_vf_regs(pdev); + } + +-/* Suppress logging when we set timeout to zeo */ ++/* Suppress logging when we set timeout to zero */ + bool mana_need_log(struct gdma_context *gc, int err) + { + struct hw_channel_context *hwc; + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- b/drivers/net/ethernet/microsoft/mana/mana_en.c ++++ b/drivers/net/ethernet/microsoft/mana/mana_en.c +@@ -863,7 +872,8 @@ + if (err == -EOPNOTSUPP) + return err; + +- if (req->req.msg_type != MANA_QUERY_PHY_STAT) ++ if (req->req.msg_type != MANA_QUERY_PHY_STAT && ++ mana_need_log(gc, err)) + dev_err(dev, "Failed to send mana message: %d, 0x%x\n", + err, resp->status); + return err ? err : -EPROTO; +--- b/include/net/mana/gdma.h ++++ b/include/net/mana/gdma.h +@@ -62,6 +62,7 @@ + GDMA_EQE_HWC_FPGA_RECONFIG = 132, + GDMA_EQE_HWC_SOC_RECONFIG_DATA = 133, + GDMA_EQE_HWC_SOC_SERVICE = 134, ++ GDMA_EQE_HWC_RESET_REQUEST = 135, + GDMA_EQE_RNIC_QP_FATAL = 176, + }; + +@@ -597,6 +601,7 @@ + GDMA_DRV_CAP_FLAG_1_VARIABLE_INDIRECTION_TABLE_SUPPORT | \ + GDMA_DRV_CAP_FLAG_1_DEV_LIST_HOLES_SUP | \ + GDMA_DRV_CAP_FLAG_1_DYNAMIC_IRQ_ALLOC_SUPPORT | \ ++ GDMA_DRV_CAP_FLAG_1_SELF_RESET_ON_EQE | \ + GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE) + + #define GDMA_DRV_CAP_FLAGS2 0 + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- b/drivers/net/ethernet/microsoft/mana/gdma_main.c ++++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c +@@ -5,6 +5,5 @@ +-#include ++#include + + #include + +-#include + struct dentry *mana_debugfs_root; +--- b/include/net/mana/gdma.h ++++ b/include/net/mana/gdma.h +@@ -58,5 +57,6 @@ + GDMA_EQE_HWC_FPGA_RECONFIG = 132, + GDMA_EQE_HWC_SOC_RECONFIG_DATA = 133, ++ GDMA_EQE_HWC_SOC_SERVICE = 134, + GDMA_EQE_RNIC_QP_FATAL = 176, + }; + +@@ -566,5 +590,6 @@ + GDMA_DRV_CAP_FLAG_1_VARIABLE_INDIRECTION_TABLE_SUPPORT | \ + GDMA_DRV_CAP_FLAG_1_DEV_LIST_HOLES_SUP | \ ++ GDMA_DRV_CAP_FLAG_1_DYNAMIC_IRQ_ALLOC_SUPPORT | \ + GDMA_DRV_CAP_FLAG_1_HANDLE_RECONFIG_EQE) + + #define GDMA_DRV_CAP_FLAGS2 0 +@@ -889,4 +915,4 @@ +-void mana_register_debugfs(void); +-void mana_unregister_debugfs(void); ++ ++int mana_rdma_service_event(struct gdma_context *gc, enum gdma_service_type event); + + #endif /* _GDMA_H */ +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy8/run-test b/tests/fuzzy8/run-test new file mode 100755 index 00000000..2dacd8ed --- /dev/null +++ b/tests/fuzzy8/run-test @@ -0,0 +1,1735 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing (using --fuzzy=3) with a real Linux kernel backport +# compared against its upstream version. Stresses having multiple relocations +# and most of the fuzzy diffing machinery as a whole. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +From b0c8e943e409740752a9a34c96743088094223e9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marcin=20Wcis=C5=82o?= +Date: Tue, 4 Nov 2025 20:51:12 +0100 +Subject: [PATCH] netfilter: nf_tables: report use refcount overflow +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +jira VULN-430 +cve-pre CVE-2023-4244 +commit-author Pablo Neira Ayuso +commit 1689f25924ada8fe14a4a82c38925d04994c7142 +upstream-diff Used the cleanly applying 9.4 backport + 854ec8345abb60f1fb65446a6aef2627f71196ca + +Overflow use refcount checks are not complete. + +Add helper function to deal with object reference counter tracking. +Report -EMFILE in case UINT_MAX is reached. + +nft_use_dec() splats in case that reference counter underflows, +which should not ever happen. + +Add nft_use_inc_restore() and nft_use_dec_restore() which are used +to restore reference counter from error and abort paths. + +Use u32 in nft_flowtable and nft_object since helper functions cannot +work on bitfields. + +Remove the few early incomplete checks now that the helper functions +are in place and used to check for refcount overflow. + +Fixes: 96518518cc41 ("netfilter: add nftables") + Signed-off-by: Pablo Neira Ayuso +(cherry picked from commit 1689f25924ada8fe14a4a82c38925d04994c7142) + Signed-off-by: Marcin Wcisło +--- + include/net/netfilter/nf_tables.h | 31 +++++- + net/netfilter/nf_tables_api.c | 163 ++++++++++++++++++------------ + net/netfilter/nft_flow_offload.c | 6 +- + net/netfilter/nft_immediate.c | 8 +- + net/netfilter/nft_objref.c | 8 +- + 5 files changed, 141 insertions(+), 75 deletions(-) + +diff --git a/include/net/netfilter/nf_tables.h b/include/net/netfilter/nf_tables.h +index ccb3b3e4ce88e..3554c8ea03d3e 100644 +--- a/include/net/netfilter/nf_tables.h ++++ b/include/net/netfilter/nf_tables.h +@@ -1145,6 +1145,29 @@ int __nft_release_basechain(struct nft_ctx *ctx); + + unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv); + ++static inline bool nft_use_inc(u32 *use) ++{ ++ if (*use == UINT_MAX) ++ return false; ++ ++ (*use)++; ++ ++ return true; ++} ++ ++static inline void nft_use_dec(u32 *use) ++{ ++ WARN_ON_ONCE((*use)-- == 0); ++} ++ ++/* For error and abort path: restore use counter to previous state. */ ++static inline void nft_use_inc_restore(u32 *use) ++{ ++ WARN_ON_ONCE(!nft_use_inc(use)); ++} ++ ++#define nft_use_dec_restore nft_use_dec ++ + /** + * struct nft_table - nf_tables table + * +@@ -1228,8 +1251,8 @@ struct nft_object { + struct list_head list; + struct rhlist_head rhlhead; + struct nft_object_hash_key key; +- u32 genmask:2, +- use:30; ++ u32 genmask:2; ++ u32 use; + u64 handle; + u16 udlen; + u8 *udata; +@@ -1331,8 +1354,8 @@ struct nft_flowtable { + char *name; + int hooknum; + int ops_len; +- u32 genmask:2, +- use:30; ++ u32 genmask:2; ++ u32 use; + u64 handle; + /* runtime data below here */ + struct list_head hook_list ____cacheline_aligned; +diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c +index 943bcc9342ea6..56291ca0d6518 100644 +--- a/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -255,8 +255,10 @@ int nf_tables_bind_chain(const struct nft_ctx *ctx, struct nft_chain *chain) + if (chain->bound) + return -EBUSY; + ++ if (!nft_use_inc(&chain->use)) ++ return -EMFILE; ++ + chain->bound = true; +- chain->use++; + nft_chain_trans_bind(ctx, chain); + + return 0; +@@ -439,7 +441,7 @@ static int nft_delchain(struct nft_ctx *ctx) + if (IS_ERR(trans)) + return PTR_ERR(trans); + +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + nft_deactivate_next(ctx->net, ctx->chain); + + return 0; +@@ -478,7 +480,7 @@ nf_tables_delrule_deactivate(struct nft_ctx *ctx, struct nft_rule *rule) + /* You cannot delete the same rule twice */ + if (nft_is_active_next(ctx->net, rule)) { + nft_deactivate_next(ctx->net, rule); +- ctx->chain->use--; ++ nft_use_dec(&ctx->chain->use); + return 0; + } + return -ENOENT; +@@ -645,7 +647,7 @@ static int nft_delset(const struct nft_ctx *ctx, struct nft_set *set) + nft_map_deactivate(ctx, set); + + nft_deactivate_next(ctx->net, set); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -677,7 +679,7 @@ static int nft_delobj(struct nft_ctx *ctx, struct nft_object *obj) + return err; + + nft_deactivate_next(ctx->net, obj); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -712,7 +714,7 @@ static int nft_delflowtable(struct nft_ctx *ctx, + return err; + + nft_deactivate_next(ctx->net, flowtable); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -2298,9 +2300,6 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + struct nft_rule **rules; + int err; + +- if (table->use == UINT_MAX) +- return -EOVERFLOW; +- + if (nla[NFTA_CHAIN_HOOK]) { + struct nft_stats __percpu *stats = NULL; + struct nft_chain_hook hook; +@@ -2397,6 +2396,11 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + if (err < 0) + goto err_destroy_chain; + ++ if (!nft_use_inc(&table->use)) { ++ err = -EMFILE; ++ goto err_use; ++ } ++ + trans = nft_trans_chain_add(ctx, NFT_MSG_NEWCHAIN); + if (IS_ERR(trans)) { + err = PTR_ERR(trans); +@@ -2413,10 +2417,11 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + goto err_unregister_hook; + } + +- table->use++; +- + return 0; ++ + err_unregister_hook: ++ nft_use_dec_restore(&table->use); ++err_use: + nf_tables_unregister_hook(net, table, chain); + err_destroy_chain: + nf_tables_chain_destroy(ctx); +@@ -3616,9 +3621,6 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + return -EINVAL; + handle = nf_tables_alloc_handle(table); + +- if (chain->use == UINT_MAX) +- return -EOVERFLOW; +- + if (nla[NFTA_RULE_POSITION]) { + pos_handle = be64_to_cpu(nla_get_be64(nla[NFTA_RULE_POSITION])); + old_rule = __nft_rule_lookup(chain, pos_handle); +@@ -3712,6 +3714,11 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + } + } + ++ if (!nft_use_inc(&chain->use)) { ++ err = -EMFILE; ++ goto err_release_rule; ++ } ++ + if (info->nlh->nlmsg_flags & NLM_F_REPLACE) { + err = nft_delrule(&ctx, old_rule); + if (err < 0) +@@ -3743,7 +3750,6 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + } + } + kvfree(expr_info); +- chain->use++; + + if (flow) + nft_trans_flow_rule(trans) = flow; +@@ -3754,6 +3760,7 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + return 0; + + err_destroy_flow_rule: ++ nft_use_dec_restore(&chain->use); + if (flow) + nft_flow_rule_destroy(flow); + err_release_rule: +@@ -4786,9 +4793,15 @@ static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info, + alloc_size = sizeof(*set) + size + udlen; + if (alloc_size < size || alloc_size > INT_MAX) + return -ENOMEM; ++ ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + set = kvzalloc(alloc_size, GFP_KERNEL); +- if (!set) +- return -ENOMEM; ++ if (!set) { ++ err = -ENOMEM; ++ goto err_alloc; ++ } + + name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL); + if (!name) { +@@ -4846,7 +4859,7 @@ static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info, + goto err_set_expr_alloc; + + list_add_tail_rcu(&set->list, &table->sets); +- table->use++; ++ + return 0; + + err_set_expr_alloc: +@@ -4858,6 +4871,9 @@ err_set_init: + kfree(set->name); + err_set_name: + kvfree(set); ++err_alloc: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -4996,9 +5012,6 @@ int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, + struct nft_set_binding *i; + struct nft_set_iter iter; + +- if (set->use == UINT_MAX) +- return -EOVERFLOW; +- + if (!list_empty(&set->bindings) && nft_set_is_anonymous(set)) + return -EBUSY; + +@@ -5026,10 +5039,12 @@ int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, + return iter.err; + } + bind: ++ if (!nft_use_inc(&set->use)) ++ return -EMFILE; ++ + binding->chain = ctx->chain; + list_add_tail_rcu(&binding->list, &set->bindings); + nft_set_trans_bind(ctx, set); +- set->use++; + + return 0; + } +@@ -5103,7 +5118,7 @@ void nf_tables_activate_set(const struct nft_ctx *ctx, struct nft_set *set) + nft_clear(ctx->net, set); + } + +- set->use++; ++ nft_use_inc_restore(&set->use); + } + EXPORT_SYMBOL_GPL(nf_tables_activate_set); + +@@ -5119,7 +5134,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + else + list_del_rcu(&binding->list); + +- set->use--; ++ nft_use_dec(&set->use); + break; + case NFT_TRANS_PREPARE: + if (nft_set_is_anonymous(set)) { +@@ -5128,7 +5143,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + + nft_deactivate_next(ctx->net, set); + } +- set->use--; ++ nft_use_dec(&set->use); + return; + case NFT_TRANS_ABORT: + case NFT_TRANS_RELEASE: +@@ -5136,7 +5151,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + set->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_deactivate(ctx, set); + +- set->use--; ++ nft_use_dec(&set->use); + fallthrough; + default: + nf_tables_unbind_set(ctx, set, binding, +@@ -5927,7 +5942,7 @@ void nft_set_elem_destroy(const struct nft_set *set, void *elem, + nft_set_elem_expr_destroy(&ctx, nft_set_ext_expr(ext)); + + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use--; ++ nft_use_dec(&(*nft_set_ext_obj(ext))->use); + kfree(elem); + } + EXPORT_SYMBOL_GPL(nft_set_elem_destroy); +@@ -6429,8 +6444,16 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, + set->objtype, genmask); + if (IS_ERR(obj)) { + err = PTR_ERR(obj); ++ obj = NULL; + goto err_parse_key_end; + } ++ ++ if (!nft_use_inc(&obj->use)) { ++ err = -EMFILE; ++ obj = NULL; ++ goto err_parse_key_end; ++ } ++ + err = nft_set_ext_add(&tmpl, NFT_SET_EXT_OBJREF); + if (err < 0) + goto err_parse_key_end; +@@ -6499,10 +6522,9 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, + if (flags) + *nft_set_ext_flags(ext) = flags; + +- if (obj) { ++ if (obj) + *nft_set_ext_obj(ext) = obj; +- obj->use++; +- } ++ + if (ulen > 0) { + if (nft_set_ext_check(&tmpl, NFT_SET_EXT_USERDATA, ulen) < 0) { + err = -EINVAL; +@@ -6567,12 +6589,13 @@ err_element_clash: + kfree(trans); + err_elem_free: + nf_tables_set_elem_destroy(ctx, set, elem.priv); +- if (obj) +- obj->use--; + err_parse_data: + if (nla[NFTA_SET_ELEM_DATA] != NULL) + nft_data_release(&elem.data.val, desc.type); + err_parse_key_end: ++ if (obj) ++ nft_use_dec_restore(&obj->use); ++ + nft_data_release(&elem.key_end.val, NFT_DATA_VALUE); + err_parse_key: + nft_data_release(&elem.key.val, NFT_DATA_VALUE); +@@ -6653,7 +6676,7 @@ void nft_data_hold(const struct nft_data *data, enum nft_data_types type) + case NFT_JUMP: + case NFT_GOTO: + chain = data->verdict.chain; +- chain->use++; ++ nft_use_inc_restore(&chain->use); + break; + } + } +@@ -6668,7 +6691,7 @@ static void nft_setelem_data_activate(const struct net *net, + if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA)) + nft_data_hold(nft_set_ext_data(ext), set->dtype); + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use++; ++ nft_use_inc_restore(&(*nft_set_ext_obj(ext))->use); + } + + static void nft_setelem_data_deactivate(const struct net *net, +@@ -6680,7 +6703,7 @@ static void nft_setelem_data_deactivate(const struct net *net, + if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA)) + nft_data_release(nft_set_ext_data(ext), set->dtype); + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use--; ++ nft_use_dec(&(*nft_set_ext_obj(ext))->use); + } + + static int nft_del_setelem(struct nft_ctx *ctx, struct nft_set *set, +@@ -7220,9 +7243,14 @@ static int nf_tables_newobj(struct sk_buff *skb, const struct nfnl_info *info, + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + type = nft_obj_type_get(net, objtype); +- if (IS_ERR(type)) +- return PTR_ERR(type); ++ if (IS_ERR(type)) { ++ err = PTR_ERR(type); ++ goto err_type; ++ } + + obj = nft_obj_init(&ctx, type, nla[NFTA_OBJ_DATA]); + if (IS_ERR(obj)) { +@@ -7256,7 +7284,7 @@ static int nf_tables_newobj(struct sk_buff *skb, const struct nfnl_info *info, + goto err_obj_ht; + + list_add_tail_rcu(&obj->list, &table->objects); +- table->use++; ++ + return 0; + err_obj_ht: + /* queued in transaction log */ +@@ -7272,6 +7300,9 @@ err_strdup: + kfree(obj); + err_init: + module_put(type->owner); ++err_type: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -7666,7 +7697,7 @@ void nf_tables_deactivate_flowtable(const struct nft_ctx *ctx, + case NFT_TRANS_PREPARE: + case NFT_TRANS_ABORT: + case NFT_TRANS_RELEASE: +- flowtable->use--; ++ nft_use_dec(&flowtable->use); + fallthrough; + default: + return; +@@ -8014,9 +8045,14 @@ static int nf_tables_newflowtable(struct sk_buff *skb, + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL); +- if (!flowtable) +- return -ENOMEM; ++ if (!flowtable) { ++ err = -ENOMEM; ++ goto flowtable_alloc; ++ } + + flowtable->table = table; + flowtable->handle = nf_tables_alloc_handle(table); +@@ -8071,7 +8107,6 @@ static int nf_tables_newflowtable(struct sk_buff *skb, + goto err5; + + list_add_tail_rcu(&flowtable->list, &table->flowtables); +- table->use++; + + return 0; + err5: +@@ -8088,6 +8123,9 @@ err2: + kfree(flowtable->name); + err1: + kfree(flowtable); ++flowtable_alloc: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -9392,7 +9430,7 @@ static int nf_tables_commit(struct net *net, struct sk_buff *skb) + */ + if (nft_set_is_anonymous(nft_trans_set(trans)) && + !list_empty(&nft_trans_set(trans)->bindings)) +- trans->ctx.table->use--; ++ nft_use_dec(&trans->ctx.table->use); + } + nf_tables_set_notify(&trans->ctx, nft_trans_set(trans), + NFT_MSG_NEWSET, GFP_KERNEL); +@@ -9616,7 +9654,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + nft_chain_del(trans->ctx.chain); + nf_tables_unregister_hook(trans->ctx.net, + trans->ctx.table, +@@ -9625,7 +9663,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + break; + case NFT_MSG_DELCHAIN: + case NFT_MSG_DESTROYCHAIN: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, trans->ctx.chain); + nft_trans_destroy(trans); + break; +@@ -9634,7 +9672,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.chain->use--; ++ nft_use_dec_restore(&trans->ctx.chain->use); + list_del_rcu(&nft_trans_rule(trans)->list); + nft_rule_expr_deactivate(&trans->ctx, + nft_trans_rule(trans), +@@ -9644,7 +9682,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + break; + case NFT_MSG_DELRULE: + case NFT_MSG_DESTROYRULE: +- trans->ctx.chain->use++; ++ nft_use_inc_restore(&trans->ctx.chain->use); + nft_clear(trans->ctx.net, nft_trans_rule(trans)); + nft_rule_expr_activate(&trans->ctx, nft_trans_rule(trans)); + if (trans->ctx.chain->flags & NFT_CHAIN_HW_OFFLOAD) +@@ -9657,7 +9695,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + if (nft_trans_set_bound(trans)) { + nft_trans_destroy(trans); + break; +@@ -9666,7 +9704,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + break; + case NFT_MSG_DELSET: + case NFT_MSG_DESTROYSET: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_set(trans)); + if (nft_trans_set(trans)->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_activate(&trans->ctx, nft_trans_set(trans)); +@@ -9710,13 +9748,13 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_obj_destroy(&trans->ctx, nft_trans_obj_newobj(trans)); + nft_trans_destroy(trans); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + nft_obj_del(nft_trans_obj(trans)); + } + break; + case NFT_MSG_DELOBJ: + case NFT_MSG_DESTROYOBJ: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_obj(trans)); + nft_trans_destroy(trans); + break; +@@ -9725,7 +9763,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable_hooks(trans)); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + list_del_rcu(&nft_trans_flowtable(trans)->list); + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable(trans)->hook_list); +@@ -9737,7 +9775,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + list_splice(&nft_trans_flowtable_hooks(trans), + &nft_trans_flowtable(trans)->hook_list); + } else { +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_flowtable(trans)); + } + nft_trans_destroy(trans); +@@ -10181,8 +10219,9 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data, + if (desc->flags & NFT_DATA_DESC_SETELEM && + chain->flags & NFT_CHAIN_BINDING) + return -EINVAL; ++ if (!nft_use_inc(&chain->use)) ++ return -EMFILE; + +- chain->use++; + data->verdict.chain = chain; + break; + default: +@@ -10202,7 +10241,7 @@ static void nft_verdict_uninit(const struct nft_data *data) + case NFT_JUMP: + case NFT_GOTO: + chain = data->verdict.chain; +- chain->use--; ++ nft_use_dec(&chain->use); + break; + } + } +@@ -10371,11 +10410,11 @@ int __nft_release_basechain(struct nft_ctx *ctx) + nf_tables_unregister_hook(ctx->net, ctx->chain->table, ctx->chain); + list_for_each_entry_safe(rule, nr, &ctx->chain->rules, list) { + list_del(&rule->list); +- ctx->chain->use--; ++ nft_use_dec(&ctx->chain->use); + nf_tables_rule_release(ctx, rule); + } + nft_chain_del(ctx->chain); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + nf_tables_chain_destroy(ctx); + + return 0; +@@ -10425,18 +10464,18 @@ static void __nft_release_table(struct net *net, struct nft_table *table) + ctx.chain = chain; + list_for_each_entry_safe(rule, nr, &chain->rules, list) { + list_del(&rule->list); +- chain->use--; ++ nft_use_dec(&chain->use); + nf_tables_rule_release(&ctx, rule); + } + } + list_for_each_entry_safe(flowtable, nf, &table->flowtables, list) { + list_del(&flowtable->list); +- table->use--; ++ nft_use_dec(&table->use); + nf_tables_flowtable_destroy(flowtable); + } + list_for_each_entry_safe(set, ns, &table->sets, list) { + list_del(&set->list); +- table->use--; ++ nft_use_dec(&table->use); + if (set->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_deactivate(&ctx, set); + +@@ -10444,13 +10483,13 @@ static void __nft_release_table(struct net *net, struct nft_table *table) + } + list_for_each_entry_safe(obj, ne, &table->objects, list) { + nft_obj_del(obj); +- table->use--; ++ nft_use_dec(&table->use); + nft_obj_destroy(&ctx, obj); + } + list_for_each_entry_safe(chain, nc, &table->chains, list) { + ctx.chain = chain; + nft_chain_del(chain); +- table->use--; ++ nft_use_dec(&table->use); + nf_tables_chain_destroy(&ctx); + } + nf_tables_table_destroy(&ctx); +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +index 6db8c802d5e76..9a05fca9c48b7 100644 +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -381,8 +381,10 @@ static int nft_flow_offload_init(const struct nft_ctx *ctx, + if (IS_ERR(flowtable)) + return PTR_ERR(flowtable); + ++ if (!nft_use_inc(&flowtable->use)) ++ return -EMFILE; ++ + priv->flowtable = flowtable; +- flowtable->use++; + + return nf_ct_netns_get(ctx->net, ctx->family); + } +@@ -401,7 +403,7 @@ static void nft_flow_offload_activate(const struct nft_ctx *ctx, + { + struct nft_flow_offload *priv = nft_expr_priv(expr); + +- priv->flowtable->use++; ++ nft_use_inc_restore(&priv->flowtable->use); + } + + static void nft_flow_offload_destroy(const struct nft_ctx *ctx, +diff --git a/net/netfilter/nft_immediate.c b/net/netfilter/nft_immediate.c +index 7c810005a1f9f..11a39289fe49b 100644 +--- a/net/netfilter/nft_immediate.c ++++ b/net/netfilter/nft_immediate.c +@@ -159,7 +159,7 @@ static void nft_immediate_deactivate(const struct nft_ctx *ctx, + default: + nft_chain_del(chain); + chain->bound = false; +- chain->table->use--; ++ nft_use_dec(&chain->table->use); + break; + } + break; +@@ -198,7 +198,7 @@ static void nft_immediate_destroy(const struct nft_ctx *ctx, + * let the transaction records release this chain and its rules. + */ + if (chain->bound) { +- chain->use--; ++ nft_use_dec(&chain->use); + break; + } + +@@ -206,9 +206,9 @@ static void nft_immediate_destroy(const struct nft_ctx *ctx, + chain_ctx = *ctx; + chain_ctx.chain = chain; + +- chain->use--; ++ nft_use_dec(&chain->use); + list_for_each_entry_safe(rule, n, &chain->rules, list) { +- chain->use--; ++ nft_use_dec(&chain->use); + list_del(&rule->list); + nf_tables_rule_destroy(&chain_ctx, rule); + } +diff --git a/net/netfilter/nft_objref.c b/net/netfilter/nft_objref.c +index e873401182899..10850266221a6 100644 +--- a/net/netfilter/nft_objref.c ++++ b/net/netfilter/nft_objref.c +@@ -41,8 +41,10 @@ static int nft_objref_init(const struct nft_ctx *ctx, + if (IS_ERR(obj)) + return -ENOENT; + ++ if (!nft_use_inc(&obj->use)) ++ return -EMFILE; ++ + nft_objref_priv(expr) = obj; +- obj->use++; + + return 0; + } +@@ -72,7 +74,7 @@ static void nft_objref_deactivate(const struct nft_ctx *ctx, + if (phase == NFT_TRANS_COMMIT) + return; + +- obj->use--; ++ nft_use_dec(&obj->use); + } + + static void nft_objref_activate(const struct nft_ctx *ctx, +@@ -80,7 +82,7 @@ static void nft_objref_activate(const struct nft_ctx *ctx, + { + struct nft_object *obj = nft_objref_priv(expr); + +- obj->use++; ++ nft_use_inc_restore(&obj->use); + } + + static struct nft_expr_type nft_objref_type; +-- +2.52.0 + +EOF + +cat << 'EOF' > patch2 +From 1689f25924ada8fe14a4a82c38925d04994c7142 Mon Sep 17 00:00:00 2001 +From: Pablo Neira Ayuso +Date: Wed, 28 Jun 2023 16:24:27 +0200 +Subject: [PATCH] netfilter: nf_tables: report use refcount overflow + +Overflow use refcount checks are not complete. + +Add helper function to deal with object reference counter tracking. +Report -EMFILE in case UINT_MAX is reached. + +nft_use_dec() splats in case that reference counter underflows, +which should not ever happen. + +Add nft_use_inc_restore() and nft_use_dec_restore() which are used +to restore reference counter from error and abort paths. + +Use u32 in nft_flowtable and nft_object since helper functions cannot +work on bitfields. + +Remove the few early incomplete checks now that the helper functions +are in place and used to check for refcount overflow. + +Fixes: 96518518cc41 ("netfilter: add nftables") +Signed-off-by: Pablo Neira Ayuso +--- + include/net/netfilter/nf_tables.h | 31 +++++- + net/netfilter/nf_tables_api.c | 163 ++++++++++++++++++------------ + net/netfilter/nft_flow_offload.c | 6 +- + net/netfilter/nft_immediate.c | 8 +- + net/netfilter/nft_objref.c | 8 +- + 5 files changed, 141 insertions(+), 75 deletions(-) + +diff --git a/include/net/netfilter/nf_tables.h b/include/net/netfilter/nf_tables.h +index 84f2fd85fd5ae..640441a2f9266 100644 +--- a/include/net/netfilter/nf_tables.h ++++ b/include/net/netfilter/nf_tables.h +@@ -1211,6 +1211,29 @@ int __nft_release_basechain(struct nft_ctx *ctx); + + unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv); + ++static inline bool nft_use_inc(u32 *use) ++{ ++ if (*use == UINT_MAX) ++ return false; ++ ++ (*use)++; ++ ++ return true; ++} ++ ++static inline void nft_use_dec(u32 *use) ++{ ++ WARN_ON_ONCE((*use)-- == 0); ++} ++ ++/* For error and abort path: restore use counter to previous state. */ ++static inline void nft_use_inc_restore(u32 *use) ++{ ++ WARN_ON_ONCE(!nft_use_inc(use)); ++} ++ ++#define nft_use_dec_restore nft_use_dec ++ + /** + * struct nft_table - nf_tables table + * +@@ -1296,8 +1319,8 @@ struct nft_object { + struct list_head list; + struct rhlist_head rhlhead; + struct nft_object_hash_key key; +- u32 genmask:2, +- use:30; ++ u32 genmask:2; ++ u32 use; + u64 handle; + u16 udlen; + u8 *udata; +@@ -1399,8 +1422,8 @@ struct nft_flowtable { + char *name; + int hooknum; + int ops_len; +- u32 genmask:2, +- use:30; ++ u32 genmask:2; ++ u32 use; + u64 handle; + /* runtime data below here */ + struct list_head hook_list ____cacheline_aligned; +diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c +index 9573a8fcad796..86b3c4de7f40d 100644 +--- a/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -253,8 +253,10 @@ int nf_tables_bind_chain(const struct nft_ctx *ctx, struct nft_chain *chain) + if (chain->bound) + return -EBUSY; + ++ if (!nft_use_inc(&chain->use)) ++ return -EMFILE; ++ + chain->bound = true; +- chain->use++; + nft_chain_trans_bind(ctx, chain); + + return 0; +@@ -437,7 +439,7 @@ static int nft_delchain(struct nft_ctx *ctx) + if (IS_ERR(trans)) + return PTR_ERR(trans); + +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + nft_deactivate_next(ctx->net, ctx->chain); + + return 0; +@@ -476,7 +478,7 @@ nf_tables_delrule_deactivate(struct nft_ctx *ctx, struct nft_rule *rule) + /* You cannot delete the same rule twice */ + if (nft_is_active_next(ctx->net, rule)) { + nft_deactivate_next(ctx->net, rule); +- ctx->chain->use--; ++ nft_use_dec(&ctx->chain->use); + return 0; + } + return -ENOENT; +@@ -644,7 +646,7 @@ static int nft_delset(const struct nft_ctx *ctx, struct nft_set *set) + nft_map_deactivate(ctx, set); + + nft_deactivate_next(ctx->net, set); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -676,7 +678,7 @@ static int nft_delobj(struct nft_ctx *ctx, struct nft_object *obj) + return err; + + nft_deactivate_next(ctx->net, obj); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -711,7 +713,7 @@ static int nft_delflowtable(struct nft_ctx *ctx, + return err; + + nft_deactivate_next(ctx->net, flowtable); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + + return err; + } +@@ -2396,9 +2398,6 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + struct nft_chain *chain; + int err; + +- if (table->use == UINT_MAX) +- return -EOVERFLOW; +- + if (nla[NFTA_CHAIN_HOOK]) { + struct nft_stats __percpu *stats = NULL; + struct nft_chain_hook hook = {}; +@@ -2494,6 +2493,11 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + if (err < 0) + goto err_destroy_chain; + ++ if (!nft_use_inc(&table->use)) { ++ err = -EMFILE; ++ goto err_use; ++ } ++ + trans = nft_trans_chain_add(ctx, NFT_MSG_NEWCHAIN); + if (IS_ERR(trans)) { + err = PTR_ERR(trans); +@@ -2510,10 +2514,11 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask, + goto err_unregister_hook; + } + +- table->use++; +- + return 0; ++ + err_unregister_hook: ++ nft_use_dec_restore(&table->use); ++err_use: + nf_tables_unregister_hook(net, table, chain); + err_destroy_chain: + nf_tables_chain_destroy(ctx); +@@ -3840,9 +3845,6 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + return -EINVAL; + handle = nf_tables_alloc_handle(table); + +- if (chain->use == UINT_MAX) +- return -EOVERFLOW; +- + if (nla[NFTA_RULE_POSITION]) { + pos_handle = be64_to_cpu(nla_get_be64(nla[NFTA_RULE_POSITION])); + old_rule = __nft_rule_lookup(chain, pos_handle); +@@ -3936,6 +3938,11 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + } + } + ++ if (!nft_use_inc(&chain->use)) { ++ err = -EMFILE; ++ goto err_release_rule; ++ } ++ + if (info->nlh->nlmsg_flags & NLM_F_REPLACE) { + err = nft_delrule(&ctx, old_rule); + if (err < 0) +@@ -3967,7 +3974,6 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + } + } + kvfree(expr_info); +- chain->use++; + + if (flow) + nft_trans_flow_rule(trans) = flow; +@@ -3978,6 +3984,7 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info, + return 0; + + err_destroy_flow_rule: ++ nft_use_dec_restore(&chain->use); + if (flow) + nft_flow_rule_destroy(flow); + err_release_rule: +@@ -5014,9 +5021,15 @@ static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info, + alloc_size = sizeof(*set) + size + udlen; + if (alloc_size < size || alloc_size > INT_MAX) + return -ENOMEM; ++ ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + set = kvzalloc(alloc_size, GFP_KERNEL_ACCOUNT); +- if (!set) +- return -ENOMEM; ++ if (!set) { ++ err = -ENOMEM; ++ goto err_alloc; ++ } + + name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL_ACCOUNT); + if (!name) { +@@ -5074,7 +5087,7 @@ static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info, + goto err_set_expr_alloc; + + list_add_tail_rcu(&set->list, &table->sets); +- table->use++; ++ + return 0; + + err_set_expr_alloc: +@@ -5086,6 +5099,9 @@ err_set_init: + kfree(set->name); + err_set_name: + kvfree(set); ++err_alloc: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -5224,9 +5240,6 @@ int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, + struct nft_set_binding *i; + struct nft_set_iter iter; + +- if (set->use == UINT_MAX) +- return -EOVERFLOW; +- + if (!list_empty(&set->bindings) && nft_set_is_anonymous(set)) + return -EBUSY; + +@@ -5254,10 +5267,12 @@ int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, + return iter.err; + } + bind: ++ if (!nft_use_inc(&set->use)) ++ return -EMFILE; ++ + binding->chain = ctx->chain; + list_add_tail_rcu(&binding->list, &set->bindings); + nft_set_trans_bind(ctx, set); +- set->use++; + + return 0; + } +@@ -5331,7 +5346,7 @@ void nf_tables_activate_set(const struct nft_ctx *ctx, struct nft_set *set) + nft_clear(ctx->net, set); + } + +- set->use++; ++ nft_use_inc_restore(&set->use); + } + EXPORT_SYMBOL_GPL(nf_tables_activate_set); + +@@ -5347,7 +5362,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + else + list_del_rcu(&binding->list); + +- set->use--; ++ nft_use_dec(&set->use); + break; + case NFT_TRANS_PREPARE: + if (nft_set_is_anonymous(set)) { +@@ -5356,7 +5371,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + + nft_deactivate_next(ctx->net, set); + } +- set->use--; ++ nft_use_dec(&set->use); + return; + case NFT_TRANS_ABORT: + case NFT_TRANS_RELEASE: +@@ -5364,7 +5379,7 @@ void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set, + set->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_deactivate(ctx, set); + +- set->use--; ++ nft_use_dec(&set->use); + fallthrough; + default: + nf_tables_unbind_set(ctx, set, binding, +@@ -6155,7 +6170,7 @@ void nft_set_elem_destroy(const struct nft_set *set, void *elem, + nft_set_elem_expr_destroy(&ctx, nft_set_ext_expr(ext)); + + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use--; ++ nft_use_dec(&(*nft_set_ext_obj(ext))->use); + kfree(elem); + } + EXPORT_SYMBOL_GPL(nft_set_elem_destroy); +@@ -6657,8 +6672,16 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, + set->objtype, genmask); + if (IS_ERR(obj)) { + err = PTR_ERR(obj); ++ obj = NULL; + goto err_parse_key_end; + } ++ ++ if (!nft_use_inc(&obj->use)) { ++ err = -EMFILE; ++ obj = NULL; ++ goto err_parse_key_end; ++ } ++ + err = nft_set_ext_add(&tmpl, NFT_SET_EXT_OBJREF); + if (err < 0) + goto err_parse_key_end; +@@ -6727,10 +6750,9 @@ static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, + if (flags) + *nft_set_ext_flags(ext) = flags; + +- if (obj) { ++ if (obj) + *nft_set_ext_obj(ext) = obj; +- obj->use++; +- } ++ + if (ulen > 0) { + if (nft_set_ext_check(&tmpl, NFT_SET_EXT_USERDATA, ulen) < 0) { + err = -EINVAL; +@@ -6798,12 +6820,13 @@ err_element_clash: + kfree(trans); + err_elem_free: + nf_tables_set_elem_destroy(ctx, set, elem.priv); +- if (obj) +- obj->use--; + err_parse_data: + if (nla[NFTA_SET_ELEM_DATA] != NULL) + nft_data_release(&elem.data.val, desc.type); + err_parse_key_end: ++ if (obj) ++ nft_use_dec_restore(&obj->use); ++ + nft_data_release(&elem.key_end.val, NFT_DATA_VALUE); + err_parse_key: + nft_data_release(&elem.key.val, NFT_DATA_VALUE); +@@ -6883,7 +6906,7 @@ void nft_data_hold(const struct nft_data *data, enum nft_data_types type) + case NFT_JUMP: + case NFT_GOTO: + chain = data->verdict.chain; +- chain->use++; ++ nft_use_inc_restore(&chain->use); + break; + } + } +@@ -6898,7 +6921,7 @@ static void nft_setelem_data_activate(const struct net *net, + if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA)) + nft_data_hold(nft_set_ext_data(ext), set->dtype); + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use++; ++ nft_use_inc_restore(&(*nft_set_ext_obj(ext))->use); + } + + static void nft_setelem_data_deactivate(const struct net *net, +@@ -6910,7 +6933,7 @@ static void nft_setelem_data_deactivate(const struct net *net, + if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA)) + nft_data_release(nft_set_ext_data(ext), set->dtype); + if (nft_set_ext_exists(ext, NFT_SET_EXT_OBJREF)) +- (*nft_set_ext_obj(ext))->use--; ++ nft_use_dec(&(*nft_set_ext_obj(ext))->use); + } + + static int nft_del_setelem(struct nft_ctx *ctx, struct nft_set *set, +@@ -7453,9 +7476,14 @@ static int nf_tables_newobj(struct sk_buff *skb, const struct nfnl_info *info, + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + type = nft_obj_type_get(net, objtype); +- if (IS_ERR(type)) +- return PTR_ERR(type); ++ if (IS_ERR(type)) { ++ err = PTR_ERR(type); ++ goto err_type; ++ } + + obj = nft_obj_init(&ctx, type, nla[NFTA_OBJ_DATA]); + if (IS_ERR(obj)) { +@@ -7489,7 +7517,7 @@ static int nf_tables_newobj(struct sk_buff *skb, const struct nfnl_info *info, + goto err_obj_ht; + + list_add_tail_rcu(&obj->list, &table->objects); +- table->use++; ++ + return 0; + err_obj_ht: + /* queued in transaction log */ +@@ -7505,6 +7533,9 @@ err_strdup: + kfree(obj); + err_init: + module_put(type->owner); ++err_type: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -7906,7 +7937,7 @@ void nf_tables_deactivate_flowtable(const struct nft_ctx *ctx, + case NFT_TRANS_PREPARE: + case NFT_TRANS_ABORT: + case NFT_TRANS_RELEASE: +- flowtable->use--; ++ nft_use_dec(&flowtable->use); + fallthrough; + default: + return; +@@ -8260,9 +8291,14 @@ static int nf_tables_newflowtable(struct sk_buff *skb, + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ + flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL_ACCOUNT); +- if (!flowtable) +- return -ENOMEM; ++ if (!flowtable) { ++ err = -ENOMEM; ++ goto flowtable_alloc; ++ } + + flowtable->table = table; + flowtable->handle = nf_tables_alloc_handle(table); +@@ -8317,7 +8353,6 @@ static int nf_tables_newflowtable(struct sk_buff *skb, + goto err5; + + list_add_tail_rcu(&flowtable->list, &table->flowtables); +- table->use++; + + return 0; + err5: +@@ -8334,6 +8369,9 @@ err2: + kfree(flowtable->name); + err1: + kfree(flowtable); ++flowtable_alloc: ++ nft_use_dec_restore(&table->use); ++ + return err; + } + +@@ -9713,7 +9751,7 @@ static int nf_tables_commit(struct net *net, struct sk_buff *skb) + */ + if (nft_set_is_anonymous(nft_trans_set(trans)) && + !list_empty(&nft_trans_set(trans)->bindings)) +- trans->ctx.table->use--; ++ nft_use_dec(&trans->ctx.table->use); + } + nf_tables_set_notify(&trans->ctx, nft_trans_set(trans), + NFT_MSG_NEWSET, GFP_KERNEL); +@@ -9943,7 +9981,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + nft_chain_del(trans->ctx.chain); + nf_tables_unregister_hook(trans->ctx.net, + trans->ctx.table, +@@ -9956,7 +9994,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + list_splice(&nft_trans_chain_hooks(trans), + &nft_trans_basechain(trans)->hook_list); + } else { +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, trans->ctx.chain); + } + nft_trans_destroy(trans); +@@ -9966,7 +10004,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.chain->use--; ++ nft_use_dec_restore(&trans->ctx.chain->use); + list_del_rcu(&nft_trans_rule(trans)->list); + nft_rule_expr_deactivate(&trans->ctx, + nft_trans_rule(trans), +@@ -9976,7 +10014,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + break; + case NFT_MSG_DELRULE: + case NFT_MSG_DESTROYRULE: +- trans->ctx.chain->use++; ++ nft_use_inc_restore(&trans->ctx.chain->use); + nft_clear(trans->ctx.net, nft_trans_rule(trans)); + nft_rule_expr_activate(&trans->ctx, nft_trans_rule(trans)); + if (trans->ctx.chain->flags & NFT_CHAIN_HW_OFFLOAD) +@@ -9989,7 +10027,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_trans_destroy(trans); + break; + } +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + if (nft_trans_set_bound(trans)) { + nft_trans_destroy(trans); + break; +@@ -9998,7 +10036,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + break; + case NFT_MSG_DELSET: + case NFT_MSG_DESTROYSET: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_set(trans)); + if (nft_trans_set(trans)->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_activate(&trans->ctx, nft_trans_set(trans)); +@@ -10042,13 +10080,13 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_obj_destroy(&trans->ctx, nft_trans_obj_newobj(trans)); + nft_trans_destroy(trans); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + nft_obj_del(nft_trans_obj(trans)); + } + break; + case NFT_MSG_DELOBJ: + case NFT_MSG_DESTROYOBJ: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_obj(trans)); + nft_trans_destroy(trans); + break; +@@ -10057,7 +10095,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable_hooks(trans)); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + list_del_rcu(&nft_trans_flowtable(trans)->list); + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable(trans)->hook_list); +@@ -10069,7 +10107,7 @@ static int __nf_tables_abort(struct net *net, enum nfnl_abort_action action) + list_splice(&nft_trans_flowtable_hooks(trans), + &nft_trans_flowtable(trans)->hook_list); + } else { +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_flowtable(trans)); + } + nft_trans_destroy(trans); +@@ -10518,8 +10556,9 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data, + if (desc->flags & NFT_DATA_DESC_SETELEM && + chain->flags & NFT_CHAIN_BINDING) + return -EINVAL; ++ if (!nft_use_inc(&chain->use)) ++ return -EMFILE; + +- chain->use++; + data->verdict.chain = chain; + break; + } +@@ -10537,7 +10576,7 @@ static void nft_verdict_uninit(const struct nft_data *data) + case NFT_JUMP: + case NFT_GOTO: + chain = data->verdict.chain; +- chain->use--; ++ nft_use_dec(&chain->use); + break; + } + } +@@ -10706,11 +10745,11 @@ int __nft_release_basechain(struct nft_ctx *ctx) + nf_tables_unregister_hook(ctx->net, ctx->chain->table, ctx->chain); + list_for_each_entry_safe(rule, nr, &ctx->chain->rules, list) { + list_del(&rule->list); +- ctx->chain->use--; ++ nft_use_dec(&ctx->chain->use); + nf_tables_rule_release(ctx, rule); + } + nft_chain_del(ctx->chain); +- ctx->table->use--; ++ nft_use_dec(&ctx->table->use); + nf_tables_chain_destroy(ctx); + + return 0; +@@ -10760,18 +10799,18 @@ static void __nft_release_table(struct net *net, struct nft_table *table) + ctx.chain = chain; + list_for_each_entry_safe(rule, nr, &chain->rules, list) { + list_del(&rule->list); +- chain->use--; ++ nft_use_dec(&chain->use); + nf_tables_rule_release(&ctx, rule); + } + } + list_for_each_entry_safe(flowtable, nf, &table->flowtables, list) { + list_del(&flowtable->list); +- table->use--; ++ nft_use_dec(&table->use); + nf_tables_flowtable_destroy(flowtable); + } + list_for_each_entry_safe(set, ns, &table->sets, list) { + list_del(&set->list); +- table->use--; ++ nft_use_dec(&table->use); + if (set->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_deactivate(&ctx, set); + +@@ -10779,13 +10818,13 @@ static void __nft_release_table(struct net *net, struct nft_table *table) + } + list_for_each_entry_safe(obj, ne, &table->objects, list) { + nft_obj_del(obj); +- table->use--; ++ nft_use_dec(&table->use); + nft_obj_destroy(&ctx, obj); + } + list_for_each_entry_safe(chain, nc, &table->chains, list) { + ctx.chain = chain; + nft_chain_del(chain); +- table->use--; ++ nft_use_dec(&table->use); + nf_tables_chain_destroy(&ctx); + } + nf_tables_table_destroy(&ctx); +diff --git a/net/netfilter/nft_flow_offload.c b/net/netfilter/nft_flow_offload.c +index 5ef9146e74ad9..ab3362c483b4a 100644 +--- a/net/netfilter/nft_flow_offload.c ++++ b/net/netfilter/nft_flow_offload.c +@@ -408,8 +408,10 @@ static int nft_flow_offload_init(const struct nft_ctx *ctx, + if (IS_ERR(flowtable)) + return PTR_ERR(flowtable); + ++ if (!nft_use_inc(&flowtable->use)) ++ return -EMFILE; ++ + priv->flowtable = flowtable; +- flowtable->use++; + + return nf_ct_netns_get(ctx->net, ctx->family); + } +@@ -428,7 +430,7 @@ static void nft_flow_offload_activate(const struct nft_ctx *ctx, + { + struct nft_flow_offload *priv = nft_expr_priv(expr); + +- priv->flowtable->use++; ++ nft_use_inc_restore(&priv->flowtable->use); + } + + static void nft_flow_offload_destroy(const struct nft_ctx *ctx, +diff --git a/net/netfilter/nft_immediate.c b/net/netfilter/nft_immediate.c +index 3d76ebfe8939b..407d7197f75bb 100644 +--- a/net/netfilter/nft_immediate.c ++++ b/net/netfilter/nft_immediate.c +@@ -159,7 +159,7 @@ static void nft_immediate_deactivate(const struct nft_ctx *ctx, + default: + nft_chain_del(chain); + chain->bound = false; +- chain->table->use--; ++ nft_use_dec(&chain->table->use); + break; + } + break; +@@ -198,7 +198,7 @@ static void nft_immediate_destroy(const struct nft_ctx *ctx, + * let the transaction records release this chain and its rules. + */ + if (chain->bound) { +- chain->use--; ++ nft_use_dec(&chain->use); + break; + } + +@@ -206,9 +206,9 @@ static void nft_immediate_destroy(const struct nft_ctx *ctx, + chain_ctx = *ctx; + chain_ctx.chain = chain; + +- chain->use--; ++ nft_use_dec(&chain->use); + list_for_each_entry_safe(rule, n, &chain->rules, list) { +- chain->use--; ++ nft_use_dec(&chain->use); + list_del(&rule->list); + nf_tables_rule_destroy(&chain_ctx, rule); + } +diff --git a/net/netfilter/nft_objref.c b/net/netfilter/nft_objref.c +index a48dd5b5d45b1..509011b1ef597 100644 +--- a/net/netfilter/nft_objref.c ++++ b/net/netfilter/nft_objref.c +@@ -41,8 +41,10 @@ static int nft_objref_init(const struct nft_ctx *ctx, + if (IS_ERR(obj)) + return -ENOENT; + ++ if (!nft_use_inc(&obj->use)) ++ return -EMFILE; ++ + nft_objref_priv(expr) = obj; +- obj->use++; + + return 0; + } +@@ -72,7 +74,7 @@ static void nft_objref_deactivate(const struct nft_ctx *ctx, + if (phase == NFT_TRANS_COMMIT) + return; + +- obj->use--; ++ nft_use_dec(&obj->use); + } + + static void nft_objref_activate(const struct nft_ctx *ctx, +@@ -80,7 +82,7 @@ static void nft_objref_activate(const struct nft_ctx *ctx, + { + struct nft_object *obj = nft_objref_priv(expr); + +- obj->use++; ++ nft_use_inc_restore(&obj->use); + } + + static const struct nft_expr_ops nft_objref_ops = { +-- +2.52.0 + +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- b/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -4794,14 +4794,12 @@ + if (alloc_size < size || alloc_size > INT_MAX) + return -ENOMEM; ++ set = kvzalloc(alloc_size, GFP_KERNEL); ++ if (!set) ++ return -ENOMEM; ++ + + if (!nft_use_inc(&table->use)) + return -EMFILE; + +- set = kvzalloc(alloc_size, GFP_KERNEL); +- if (!set) { +- err = -ENOMEM; +- goto err_alloc; +- } +- + name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL); + if (!name) { +@@ -8045,14 +8043,12 @@ + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + +- if (!nft_use_inc(&table->use)) +- return -EMFILE; +- + flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL); +- if (!flowtable) { +- err = -ENOMEM; +- goto flowtable_alloc; +- } ++ if (!flowtable) ++ return -ENOMEM; + + flowtable->table = table; + flowtable->handle = nf_tables_alloc_handle(table); ++ if (!nft_use_inc(&table->use)) ++ return -EMFILE; ++ +@@ -9663,7 +9659,7 @@ + break; + case NFT_MSG_DELCHAIN: + case NFT_MSG_DESTROYCHAIN: +- nft_use_inc_restore(&trans->ctx.table->use); ++ trans->ctx.table->use++; + nft_clear(trans->ctx.net, trans->ctx.chain); + nft_trans_destroy(trans); + break; + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- b/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -5026,8 +5037,10 @@ + if (alloc_size < size || alloc_size > INT_MAX) + return -ENOMEM; + set = kvzalloc(alloc_size, GFP_KERNEL_ACCOUNT); +- if (!set) +- return -ENOMEM; ++ if (!set) { ++ err = -ENOMEM; ++ goto err_alloc; ++ } + + name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL_ACCOUNT); + if (!name) { +@@ -8293,8 +8327,10 @@ + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + + flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL_ACCOUNT); +- if (!flowtable) +- return -ENOMEM; ++ if (!flowtable) { ++ err = -ENOMEM; ++ goto flowtable_alloc; ++ } + + flowtable->table = table; + flowtable->handle = nf_tables_alloc_handle(table); +@@ -10000,7 +10038,7 @@ + nft_trans_destroy(trans); + break; + } +- trans->ctx.chain->use--; ++ nft_use_dec_restore(&trans->ctx.chain->use); + list_del_rcu(&nft_trans_rule(trans)->list); + nft_rule_expr_deactivate(&trans->ctx, + nft_trans_rule(trans), +@@ -10010,7 +10048,7 @@ + break; + case NFT_MSG_DELRULE: + case NFT_MSG_DESTROYRULE: +- trans->ctx.chain->use++; ++ nft_use_inc_restore(&trans->ctx.chain->use); + nft_clear(trans->ctx.net, nft_trans_rule(trans)); + nft_rule_expr_activate(&trans->ctx, nft_trans_rule(trans)); + if (trans->ctx.chain->flags & NFT_CHAIN_HW_OFFLOAD) +@@ -10023,7 +10061,7 @@ + nft_trans_destroy(trans); + break; + } +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + if (nft_trans_set_bound(trans)) { + nft_trans_destroy(trans); + break; +@@ -10032,7 +10070,7 @@ + break; + case NFT_MSG_DELSET: + case NFT_MSG_DESTROYSET: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_set(trans)); + if (nft_trans_set(trans)->flags & (NFT_SET_MAP | NFT_SET_OBJECT)) + nft_map_activate(&trans->ctx, nft_trans_set(trans)); +@@ -10076,7 +10114,7 @@ + nft_obj_destroy(&trans->ctx, nft_trans_obj_newobj(trans)); + nft_trans_destroy(trans); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + nft_obj_del(nft_trans_obj(trans)); + } + break; +@@ -10080,7 +10118,7 @@ + break; + case NFT_MSG_DELOBJ: + case NFT_MSG_DESTROYOBJ: +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_obj(trans)); + nft_trans_destroy(trans); + break; +@@ -10091,7 +10129,7 @@ + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable_hooks(trans)); + } else { +- trans->ctx.table->use--; ++ nft_use_dec_restore(&trans->ctx.table->use); + list_del_rcu(&nft_trans_flowtable(trans)->list); + nft_unregister_flowtable_net_hooks(net, + &nft_trans_flowtable(trans)->hook_list); +@@ -10103,7 +10141,7 @@ + list_splice(&nft_trans_flowtable_hooks(trans), + &nft_trans_flowtable(trans)->hook_list); + } else { +- trans->ctx.table->use++; ++ nft_use_inc_restore(&trans->ctx.table->use); + nft_clear(trans->ctx.net, nft_trans_flowtable(trans)); + } + nft_trans_destroy(trans); + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- b/net/netfilter/nf_tables_api.c ++++ b/net/netfilter/nf_tables_api.c +@@ -4785,7 +5005,7 @@ + return -ENOMEM; +- set = kvzalloc(alloc_size, GFP_KERNEL); ++ set = kvzalloc(alloc_size, GFP_KERNEL_ACCOUNT); + if (!set) + return -ENOMEM; + +- name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL); ++ name = nla_strdup(nla[NFTA_SET_NAME], GFP_KERNEL_ACCOUNT); + if (!name) { +@@ -8011,7 +8251,7 @@ + + nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); + +- flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL); ++ flowtable = kzalloc(sizeof(*flowtable), GFP_KERNEL_ACCOUNT); + if (!flowtable) + return -ENOMEM; + +@@ -9620,6 +9947,7 @@ +- break; +- case NFT_MSG_DELCHAIN: +- case NFT_MSG_DESTROYCHAIN: +- trans->ctx.table->use++; +- nft_clear(trans->ctx.net, trans->ctx.chain); ++ list_splice(&nft_trans_chain_hooks(trans), ++ &nft_trans_basechain(trans)->hook_list); ++ } else { ++ trans->ctx.table->use++; ++ nft_clear(trans->ctx.net, trans->ctx.chain); ++ } + nft_trans_destroy(trans); +--- b/net/netfilter/nft_objref.c ++++ b/net/netfilter/nft_objref.c +@@ -83,4 +83,4 @@ + obj->use++; + } + +-static struct nft_expr_type nft_objref_type; ++static const struct nft_expr_ops nft_objref_ops = { +EOF + +${INTERDIFF} --fuzzy=3 patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 diff --git a/tests/fuzzy9/run-test b/tests/fuzzy9/run-test new file mode 100755 index 00000000..d2a373f2 --- /dev/null +++ b/tests/fuzzy9/run-test @@ -0,0 +1,293 @@ +#!/bin/sh + +# This is an interdiff(1) testcase. +# Test: Fuzzy diffing (using --fuzzy) with a real Linux kernel backport compared +# against its upstream version. Stresses having multiple relocations and most of +# the fuzzy diffing machinery as a whole. + + +. ${top_srcdir-.}/tests/common.sh + +cat << 'EOF' > patch1 +From 132eaaa4a021d14a93fcec81921723d4d034b693 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marcin=20Wcis=C5=82o?= +Date: Sat Nov 15 01:52:19 2025 +0100 +Subject: [PATCH] wifi: cfg80211: check A-MSDU format more carefully +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +jira VULN-5183 +cve CVE-2024-35937 +commit-author Johannes Berg +commit 9ad7974856926129f190ffbe3beea78460b3b7cc +upstream-diff | + 1. All changes to the `ieee80211_is_valid_amsdu' function were discarded + because it's missing from `ciqlts9_2'. + 2. Changes to `ieee80211_amsdu_to_8023s' were adapted to account for the + missing 986e43b19ae9176093da35e0a844e65c8bf9ede7 from `ciqlts9_2' + history: the `copy_len > remaining' condition was changed to + `sizeof(eth) > remaining', as `sizeof(eth)' is the only possible + value `copy_len' could have assumed in `ciqlts9_2' if it was + introduced without backporting 986e43b (pointless). + +If it looks like there's another subframe in the A-MSDU +but the header isn't fully there, we can end up reading +data out of bounds, only to discard later. Make this a +bit more careful and check if the subframe header can +even be present. + + Reported-by: syzbot+d050d437fe47d479d210@syzkaller.appspotmail.com +Link: https://msgid.link/20240226203405.a731e2c95e38.I82ce7d8c0cc8970ce29d0a39fdc07f1ffc425be4@changeid + Signed-off-by: Johannes Berg +(cherry picked from commit 9ad7974856926129f190ffbe3beea78460b3b7cc) + Signed-off-by: Marcin Wcisło +--- + net/wireless/util.c | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/net/wireless/util.c b/net/wireless/util.c +index 39680e7bad45a..582b9dbde01fe 100644 +--- a/net/wireless/util.c ++++ b/net/wireless/util.c +@@ -757,24 +757,27 @@ void ieee80211_amsdu_to_8023s(struct sk_buff *skb, struct sk_buff_head *list, + struct sk_buff *frame = NULL; + u16 ethertype; + u8 *payload; +- int offset = 0, remaining; ++ int offset = 0; + struct ethhdr eth; + bool reuse_frag = skb->head_frag && !skb_has_frag_list(skb); + bool reuse_skb = false; + bool last = false; + + while (!last) { ++ int remaining = skb->len - offset; + unsigned int subframe_len; + int len; + u8 padding; + ++ if (sizeof(eth) > remaining) ++ goto purge; ++ + skb_copy_bits(skb, offset, ð, sizeof(eth)); + len = ntohs(eth.h_proto); + subframe_len = sizeof(struct ethhdr) + len; + padding = (4 - subframe_len) & 0x3; + + /* the last MSDU has no padding */ +- remaining = skb->len - offset; + if (subframe_len > remaining) + goto purge; + /* mitigate A-MSDU aggregation injection attacks */ +EOF + +cat << 'EOF' > patch2 +From 9ad7974856926129f190ffbe3beea78460b3b7cc Mon Sep 17 00:00:00 2001 +From: Johannes Berg +Date: Mon, 26 Feb 2024 20:34:06 +0100 +Subject: [PATCH] wifi: cfg80211: check A-MSDU format more carefully + +If it looks like there's another subframe in the A-MSDU +but the header isn't fully there, we can end up reading +data out of bounds, only to discard later. Make this a +bit more careful and check if the subframe header can +even be present. + +Reported-by: syzbot+d050d437fe47d479d210@syzkaller.appspotmail.com +Link: https://msgid.link/20240226203405.a731e2c95e38.I82ce7d8c0cc8970ce29d0a39fdc07f1ffc425be4@changeid +Signed-off-by: Johannes Berg +--- + net/wireless/util.c | 14 ++++++++++---- + 1 file changed, 10 insertions(+), 4 deletions(-) + +diff --git a/net/wireless/util.c b/net/wireless/util.c +index 379f742fd7415..2bde8a3546313 100644 +--- a/net/wireless/util.c ++++ b/net/wireless/util.c +@@ -791,15 +791,19 @@ ieee80211_amsdu_subframe_length(void *field, u8 mesh_flags, u8 hdr_type) + + bool ieee80211_is_valid_amsdu(struct sk_buff *skb, u8 mesh_hdr) + { +- int offset = 0, remaining, subframe_len, padding; ++ int offset = 0, subframe_len, padding; + + for (offset = 0; offset < skb->len; offset += subframe_len + padding) { ++ int remaining = skb->len - offset; + struct { + __be16 len; + u8 mesh_flags; + } hdr; + u16 len; + ++ if (sizeof(hdr) > remaining) ++ return false; ++ + if (skb_copy_bits(skb, offset + 2 * ETH_ALEN, &hdr, sizeof(hdr)) < 0) + return false; + +@@ -807,7 +811,6 @@ bool ieee80211_is_valid_amsdu(struct sk_buff *skb, u8 mesh_hdr) + mesh_hdr); + subframe_len = sizeof(struct ethhdr) + len; + padding = (4 - subframe_len) & 0x3; +- remaining = skb->len - offset; + + if (subframe_len > remaining) + return false; +@@ -825,7 +828,7 @@ void ieee80211_amsdu_to_8023s(struct sk_buff *skb, struct sk_buff_head *list, + { + unsigned int hlen = ALIGN(extra_headroom, 4); + struct sk_buff *frame = NULL; +- int offset = 0, remaining; ++ int offset = 0; + struct { + struct ethhdr eth; + uint8_t flags; +@@ -839,10 +842,14 @@ void ieee80211_amsdu_to_8023s(struct sk_buff *skb, struct sk_buff_head *list, + copy_len = sizeof(hdr); + + while (!last) { ++ int remaining = skb->len - offset; + unsigned int subframe_len; + int len, mesh_len = 0; + u8 padding; + ++ if (copy_len > remaining) ++ goto purge; ++ + skb_copy_bits(skb, offset, &hdr, copy_len); + if (iftype == NL80211_IFTYPE_MESH_POINT) + mesh_len = __ieee80211_get_mesh_hdrlen(hdr.flags); +@@ -852,7 +859,6 @@ void ieee80211_amsdu_to_8023s(struct sk_buff *skb, struct sk_buff_head *list, + padding = (4 - subframe_len) & 0x3; + + /* the last MSDU has no padding */ +- remaining = skb->len - offset; + if (subframe_len > remaining) + goto purge; + /* mitigate A-MSDU aggregation injection attacks */ +-- +2.52.0 + +EOF + +cat << 'EOF' > expected +================================================================================ +* DELTA DIFFERENCES - code changes that differ between the patches * +================================================================================ + +--- b/net/wireless/util.c ++++ b/net/wireless/util.c +@@ -769,9 +769,6 @@ + int len; + u8 padding; + +- if (sizeof(eth) > remaining) +- goto purge; +- + skb_copy_bits(skb, offset, ð, sizeof(eth)); + len = ntohs(eth.h_proto); + subframe_len = sizeof(struct ethhdr) + len; + +################################################################################ +! REJECTED PATCH2 HUNKS - could not be compared; manual review needed ! +################################################################################ + +--- b/net/wireless/util.c ++++ b/net/wireless/util.c +@@ -791,7 +791,7 @@ + + bool ieee80211_is_valid_amsdu(struct sk_buff *skb, u8 mesh_hdr) + { +- int offset = 0, remaining, subframe_len, padding; ++ int offset = 0, subframe_len, padding; + + for (offset = 0; offset < skb->len; offset += subframe_len + padding) { + struct { +@@ -794,6 +794,7 @@ + int offset = 0, remaining, subframe_len, padding; + + for (offset = 0; offset < skb->len; offset += subframe_len + padding) { ++ int remaining = skb->len - offset; + struct { + __be16 len; + u8 mesh_flags; +@@ -797,6 +798,9 @@ + } hdr; + u16 len; + ++ if (sizeof(hdr) > remaining) ++ return false; ++ + if (skb_copy_bits(skb, offset + 2 * ETH_ALEN, &hdr, sizeof(hdr)) < 0) + return false; + +@@ -807,7 +811,6 @@ + mesh_hdr); + subframe_len = sizeof(struct ethhdr) + len; + padding = (4 - subframe_len) & 0x3; +- remaining = skb->len - offset; + + if (subframe_len > remaining) + return false; +@@ -825,7 +828,7 @@ + { + unsigned int hlen = ALIGN(extra_headroom, 4); + struct sk_buff *frame = NULL; +- int offset = 0, remaining; ++ int offset = 0; + struct { + struct ethhdr eth; + uint8_t flags; +@@ -843,6 +847,9 @@ + int len, mesh_len = 0; + u8 padding; + ++ if (copy_len > remaining) ++ goto purge; ++ + skb_copy_bits(skb, offset, &hdr, copy_len); + if (iftype == NL80211_IFTYPE_MESH_POINT) + mesh_len = __ieee80211_get_mesh_hdrlen(hdr.flags); + +================================================================================ +* CONTEXT DIFFERENCES - surrounding code differences between the patches * +================================================================================ + +--- b/net/wireless/util.c ++++ b/net/wireless/util.c +@@ -754,8 +756,5 @@ + struct sk_buff *frame = NULL; +- u16 ethertype; +- u8 *payload; + int offset = 0, remaining; +- struct ethhdr eth; +- bool reuse_frag = skb->head_frag && !skb_has_frag_list(skb); +- bool reuse_skb = false; +- bool last = false; ++ struct { ++ struct ethhdr eth; ++ uint8_t flags; +@@ -762,12 +762,12 @@ + + while (!last) { + unsigned int subframe_len; +- int len; ++ int len, mesh_len = 0; + u8 padding; + +- skb_copy_bits(skb, offset, ð, sizeof(eth)); +- len = ntohs(eth.h_proto); +- subframe_len = sizeof(struct ethhdr) + len; ++ skb_copy_bits(skb, offset, &hdr, copy_len); ++ if (iftype == NL80211_IFTYPE_MESH_POINT) ++ mesh_len = __ieee80211_get_mesh_hdrlen(hdr.flags); + padding = (4 - subframe_len) & 0x3; + + /* the last MSDU has no padding */ +EOF + +${INTERDIFF} --fuzzy patch1 patch2 2>errors >output +[ -s errors ] && exit 1 + +cmp output expected || exit 1 From 24c5b455ebf227522e2e5ac30ee1e6742153b0f8 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 19:47:35 -0800 Subject: [PATCH 27/28] tests: Add stress-test script for fuzzy mode Add tests/stress-test-fuzzy.sh which mass-tests interdiff --fuzzy by comparing every commit in a git range against itself. Since both inputs are identical, any output indicates a bug. The script runs commits in parallel and reports only failures. Usage: ./tests/stress-test-fuzzy.sh Assisted-by: Claude Opus 4.6 (1M context) --- README | 21 +++++++++++++++++ tests/stress-test-fuzzy.sh | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100755 tests/stress-test-fuzzy.sh diff --git a/README b/README index bc2f2968..eff049d7 100644 --- a/README +++ b/README @@ -91,6 +91,27 @@ Run a single test: make check TESTS=tests/addhunk1/run-test ``` +### Stress-testing fuzzy mode + +The `tests/stress-test-fuzzy.sh` script mass-tests `interdiff --fuzzy` +by comparing every commit in a git range against itself. Since both +inputs are identical, any output from interdiff indicates a bug. This +is effective against any git repository, but large repositories like +the Linux kernel are particularly useful because they exercise a wide +variety of patch shapes: hunks at line 1 with no pre-context, files +ending without a newline, file creations and deletions, mode-only +changes, multi-thousand-line diffs, whitespace-damaged patches, hunks +with duplicate context lines, and so on. + +```bash +INTERDIFF=./src/interdiff ./tests/stress-test-fuzzy.sh /path/to/linux v2.6.12-rc2..v6.19 +``` + +Only commits that produce non-empty output, a non-zero exit code, or a +timeout are printed. The `JOBS`, `TIMEOUT`, and `INTERDIFF` environment +variables can be used to tune parallelism, per-commit timeout (default +30s), and the interdiff binary path respectively. + ## Requirements - A C compiler (GCC recommended) diff --git a/tests/stress-test-fuzzy.sh b/tests/stress-test-fuzzy.sh new file mode 100755 index 00000000..8f341255 --- /dev/null +++ b/tests/stress-test-fuzzy.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Stress-test interdiff --fuzzy by comparing every commit in a range +# against itself. Any non-empty output indicates a bug. +# +# Usage: ./test-commit-range.sh +# +# Output: only prints SHAs that produce non-empty interdiff output, +# along with the first line of that output for triage. + +set -uo pipefail + +if [ $# -ne 2 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +GIT_TREE="$1" +COMMIT_RANGE="$2" +INTERDIFF="${INTERDIFF:-interdiff}" +JOBS="${JOBS:-$(nproc)}" +TIMEOUT="${TIMEOUT:-30}" + +test_commit () { + local sha="$1" + local patch out + + if ! patch=$(git -C "$GIT_TREE" format-patch -1 --stdout "$sha" 2>&1); then + echo "FAIL $sha format-patch: $patch" + return + fi + + local rc=0 + out=$(timeout "$TIMEOUT" "$INTERDIFF" --fuzzy <(echo "$patch") <(echo "$patch") 2>&1) || rc=$? + + if [ "$rc" -eq 124 ]; then + echo "FAIL $sha timed out after ${TIMEOUT}s" + elif [ "$rc" -ne 0 ]; then + echo "FAIL $sha exit $rc: $(echo "$out" | head -1)" + elif [ -n "$out" ]; then + echo "FAIL $sha $(echo "$out" | head -1)" + fi +} + +export -f test_commit +export GIT_TREE INTERDIFF TIMEOUT + +git -C "$GIT_TREE" rev-list "$COMMIT_RANGE" | xargs -P "$JOBS" -I{} bash -c 'test_commit "$@"' _ {} From b0090afd47557515b423e05be66494a4d5429fad Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 23 Feb 2026 21:36:50 -0800 Subject: [PATCH 28/28] interdiff: Fix memory leak of parsed filename on early exit When a '---' header is parsed but the following line is not '+++' or hits EOF, the allocated filename from the '---' line is leaked. Free it on both early-exit paths, matching the pattern already used in index_patch_generic(). Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 7f02a827..3e97c13c 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -3870,11 +3870,15 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) names[0] = filename_from_header (line + 4); - if (getline (&line, &linelen, p1) == -1) + if (getline (&line, &linelen, p1) == -1) { + free (names[0]); break; + } - if (strncmp (line, "+++ ", 4)) + if (strncmp (line, "+++ ", 4)) { + free (names[0]); continue; + } names[1] = filename_from_header (line + 4);