From ce948adbe6c19c7df4b908ec12c02fd187d086fc Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 17 Oct 2025 14:46:14 -0700 Subject: [PATCH 01/20] 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 dcd431d0fba1c597365d506edb1f5f873ebeea7d Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 30 Oct 2025 17:25:23 -0700 Subject: [PATCH 02/20] 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 430bbfce143e89725fb23d8f9d229a0873aa91a2 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 13 Nov 2025 17:36:12 -0800 Subject: [PATCH 03/20] 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 07ecce5faf0ab9384ff632fb006149507f11ddf9 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 14 Nov 2025 09:19:49 -0800 Subject: [PATCH 04/20] 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 69d0479662e19d1bd4a3aaf93c695ccef0b675fc Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:35:28 -0800 Subject: [PATCH 05/20] 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 3aa81a39747a4447b0295420f9cf24f0bcc2414a Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Tue, 2 Dec 2025 09:21:21 -0800 Subject: [PATCH 06/20] 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 5a167c39c3f76953ecc4eddbb7f1c57c4e4f14ed Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Tue, 2 Dec 2025 10:39:45 -0800 Subject: [PATCH 07/20] 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 c3bbb8ae1be57e30899c66139e1aaa739cddd61d Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 23 Jan 2026 16:27:34 -0800 Subject: [PATCH 08/20] 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 e260bd231326fd0cf79f6f9fbd2dfd2209478ae2 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Mon, 22 Dec 2025 17:43:05 -0800 Subject: [PATCH 09/20] 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 3edaecc1f6c356ec97699f06030dd0c8233b93c6 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 15 Jan 2026 17:29:54 -0800 Subject: [PATCH 10/20] 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 f5ea809f2f9d671fd6ab3885abcca57ec815c7ca Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 23 Jan 2026 17:03:55 -0800 Subject: [PATCH 11/20] 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 faece223dbe335ecb3371ad2e9383483be7b91be Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 4 Feb 2026 14:50:05 -0800 Subject: [PATCH 12/20] 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. Co-authored-by: Claude Opus 4.5 --- src/interdiff.c | 92 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 1970396a..065c37e5 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -11,7 +11,7 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License @@ -1146,7 +1146,7 @@ trim_context (FILE *f /* positioned at start of @@ line */, { /* For each hunk, trim the context so that the number of * pre-context lines does not exceed the number of - * post-context lines. See the fuzz1 test case. */ + * post-context lines. See the fuzz1 test case. */ char *line = NULL; size_t linelen; @@ -2129,6 +2129,53 @@ 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 +2325,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 @@ -2327,9 +2354,9 @@ output_delta (FILE *p1, FILE *p2, FILE *out) 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 + /* Uh-oh. We're trying to output a * line that made up (we never saw the - * original). As long as this is at + * original). As long as this is at * the end of a hunk we can safely * drop it (done in trim_context * later). */ @@ -2379,9 +2406,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 { @@ -2552,21 +2580,21 @@ index_patch2 (FILE *p2) return index_patch_generic (p2, &files_in_patch2, 1); } -/* With flipdiff we have two patches we want to reorder. The +/* With flipdiff we have two patches we want to reorder. The * algorithm is: * * 1. Reconstruct the file as it looks after patch1, using the context * from patch2 as well. * * 2. Apply patch2, in order to reconstruct the file as it looks after - * both patches have been applied. Write this out twice. + * both patches have been applied. Write this out twice. * * 3. Analyse patch2, taking note of the additions and subtractions it * makes. * * 4. To one of the copies of the reconstructed final image, undo the * changes from patch1 by analysing the patch line by line - * (ourselves!). Need to take account of offsets due to patch2, and + * (ourselves!). Need to take account of offsets due to patch2, and * the fact that patch2 may have changed some lines. */ struct offset { unsigned long line; /* line number after patch1, before patch2 */ @@ -2855,7 +2883,7 @@ flipdiff (FILE *p1, FILE *p2, FILE *flip1, FILE *flip2) "re-generate them first", clash, clash == 1 ? "" : "s"); - /* Now we have all the context we're going to get. Write out + /* Now we have all the context we're going to get. Write out * the file and apply patch1 in reverse, so we end up with the * file as it should look before applying patches. */ tmpfd = xmkstemp (tmpp1); From 089f677e9cf603120d7270e6630f8c08715c02bc Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:44:45 -0800 Subject: [PATCH 13/20] interdiff: Separate delta and context differences in fuzzy mode --- src/interdiff.c | 787 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 641 insertions(+), 146 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 065c37e5..9ec8f82b 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,7 +1203,6 @@ 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. */ @@ -1173,16 +1217,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 +1237,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; @@ -1255,12 +1293,7 @@ trim_context (FILE *f /* positioned at start of @@ line */, 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); - } + fputc ('\n', out); while (total_count--) { enum line_type type; @@ -1300,82 +1333,75 @@ trim_context (FILE *f /* positioned at start of @@ line */, return 0; } +/* 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; + char *line = NULL; + size_t linelen; - 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 any --- / +++ headers */ + if (skip_headers) { + while (getline (&line, &linelen, rec->hunks) > 0) { + if (!strncmp (line, "@@ ", 3)) { + fseek (rec->hunks, + -(long) strlen (line), + SEEK_CUR); + break; + } + } + } -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); } @@ -1523,7 +1549,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'; @@ -2176,6 +2201,376 @@ 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 GNU patch 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) +{ + size_t i; + + 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 (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 (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 = NULL; + int *d_filtered, has_output = 0; + size_t nrej, ndelta; + long *rej_assigned; + FILE *out; + + nrej = parse_hunks (rej, &rej_hunks, NULL); + ndelta = parse_hunks (delta, &delta_hunks, &raw_hunks); + + if (!nrej || !ndelta) + error (EXIT_FAILURE, 0, + "filter_inverted_rejects: no hunks parsed"); + + rej_assigned = xmalloc (nrej * sizeof (*rej_assigned)); + d_filtered = xmalloc (ndelta * sizeof (*d_filtered)); + memset (d_filtered, 0, ndelta * sizeof (*d_filtered)); + + /* 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 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) + continue; + + if (!rej_matches_delta_at (&rej_hunks[r], + &delta_hunks[d], + &pos_del, &pos_add)) + break; + } + + if (pos_del == delta_hunks[d].del.len && + pos_add == delta_hunks[d].add.len) + d_filtered[d] = 1; + } + + free (rej_assigned); + + out = xtmpfile (); + for (size_t d = 0; d < ndelta; d++) { + if (!d_filtered[d]) { + fwrite (raw_hunks[d].s, raw_hunks[d].len, 1, out); + has_output = 1; + } + } + + free (d_filtered); + for (size_t d = 0; d < ndelta; d++) + free (raw_hunks[d].s); + free (raw_hunks); + free_hunk_lines (rej_hunks, nrej); + free_hunk_lines (delta_hunks, ndelta); + + + if (!has_output) { + fclose (out); + return NULL; + } + + 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 *orig, *diff = run_diff (options, file1, file2, &child); + if (diff) { + orig = diff; + diff = split_patch_hunks (diff, 0, NULL, NULL, unline); + fclose (orig); + } + 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) { @@ -2184,20 +2579,20 @@ 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 has_rejects = 0, ctx_ret = 0; char *oldname = NULL, *newname = NULL; pid_t child; - FILE *in; + FILE *in = NULL; size_t namelen; long pos1 = ftell (p1), pos2 = ftell (p2); long pristine1, pristine2; long start1, start2; char options[100]; - int diff_is_empty = 1; pristine1 = ftell (p1); pristine2 = ftell (p2); @@ -2270,44 +2665,116 @@ 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; + FILE *sp, *delta_diff, *ctx_diff; + char *patch1_new_file, *ctx_patch1_orig_file; + struct lines_info patch1_new_info = {}; + 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); + has_rejects = 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 (has_rejects && 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); @@ -2323,35 +2790,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 @@ -2369,16 +2819,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) { @@ -2406,23 +2857,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, has_rejects); + 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: @@ -2432,8 +2891,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, has_rejects); + fuzzy_cleanup (tmpp2, 0); } } if (human_readable) @@ -3235,6 +3694,42 @@ 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)) { + FILE *out = (mode == mode_flip) ? flip1 : stdout; + int printed = 0; + + if (fuzzy_delta_files.head) { + fprintf (out, DELTA_DIFF_HEADER); + fuzzy_output_list (&fuzzy_delta_files, 0, out); + printed = 1; + } + + if (fuzzy_delta_rej_files.head) { + if (printed) + fputc ('\n', out); + fprintf (out, DELTA_REJ_HEADER); + fuzzy_output_list (&fuzzy_delta_rej_files, 1, out); + printed = 1; + } + + if (fuzzy_ctx_files.head) { + if (printed) + fputc ('\n', out); + fprintf (out, CONTEXT_DIFF_HEADER); + fuzzy_output_list (&fuzzy_ctx_files, 0, out); + printed = 1; + } + + if (printed) + fputc ('\n', out); + + 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 2b32e73f11605174bca2cb3d905a3508c6743b3c Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:44:54 -0800 Subject: [PATCH 14/20] interdiff: Show files only in one patch under fuzzy mode headers --- src/interdiff.c | 97 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 9ec8f82b..47ecb69e 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); @@ -1405,6 +1417,36 @@ fuzzy_output_list (struct fuzzy_file_list *list, int skip_headers, FILE *out) free (line); } +/* 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, @@ -2201,7 +2243,6 @@ 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) { @@ -2527,7 +2568,6 @@ filter_inverted_rejects (FILE *delta, FILE *rej) free_hunk_lines (rej_hunks, nrej); free_hunk_lines (delta_hunks, ndelta); - if (!has_output) { fclose (out); return NULL; @@ -2911,6 +2951,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; @@ -2920,10 +2961,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; @@ -3674,9 +3723,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) @@ -3696,7 +3752,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)) { FILE *out = (mode == mode_flip) ? flip1 : stdout; int printed = 0; @@ -3722,6 +3779,24 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) printed = 1; } + if (fuzzy_only_in_patch1) { + if (printed) + fputc ('\n', out); + fprintf (out, ONLY_IN_PATCH1_HEADER); + colorize_diff (fuzzy_only_in_patch1, out); + fclose (fuzzy_only_in_patch1); + printed = 1; + } + + if (fuzzy_only_in_patch2) { + if (printed) + fputc ('\n', out); + fprintf (out, ONLY_IN_PATCH2_HEADER); + colorize_diff (fuzzy_only_in_patch2, out); + fclose (fuzzy_only_in_patch2); + printed = 1; + } + if (printed) fputc ('\n', out); From 2776b58090e123baa8ece6f2dcc18e3d339c4017 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:38:51 -0800 Subject: [PATCH 15/20] 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 47ecb69e..b1d1df0e 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -2576,6 +2576,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; + FILE *out = xtmpfile (); + size_t nlines = 0, fsz; + + /* 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 (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; + } + + nlines = 0; + atat = (typeof (atat)){ line }; + } + + free (lines); + free (fbuf); + + if (!ftell (out)) { + fclose (out); + return NULL; + } + + rewind (out); + return out; +} + /* Run diff and filter out bogus hunks containing unlines. Returns NULL if * the resulting diff is empty. */ static FILE * @@ -2801,6 +2920,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 0c46ac1064ceb304d083ea751d70ab2b1e987628 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Wed, 18 Feb 2026 18:48:46 -0800 Subject: [PATCH 16/20] 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 | 48 +++++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index b1d1df0e..80ded5dd 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1382,6 +1382,18 @@ fuzzy_free_list (struct fuzzy_file_list *list) } } +/* 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; +} + /* 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). */ @@ -1389,8 +1401,6 @@ static void fuzzy_output_list (struct fuzzy_file_list *list, int skip_headers, FILE *out) { struct fuzzy_file_record *rec; - char *line = NULL; - size_t linelen; for (rec = list->head; rec; rec = rec->next) { if (!rec->hunks) @@ -1398,23 +1408,14 @@ fuzzy_output_list (struct fuzzy_file_list *list, int skip_headers, FILE *out) rewind (rec->hunks); - /* Skip past any --- / +++ headers */ - if (skip_headers) { - while (getline (&line, &linelen, rec->hunks) > 0) { - if (!strncmp (line, "@@ ", 3)) { - fseek (rec->hunks, - -(long) strlen (line), - SEEK_CUR); - break; - } - } - } + /* Skip past --- / +++ headers */ + if (skip_headers && skip_header_lines (rec->hunks)) + error (EXIT_FAILURE, 0, "truncated hunk file"); 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); } /* Colorize and output a raw diff (with --- / +++ / @@ headers) */ @@ -2205,7 +2206,6 @@ run_diff (const char *options, const char *file1, const char *file2, { pid_t child; FILE *in; - int diff_is_empty = 1; fflush (NULL); @@ -2217,24 +2217,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 49b397be90ffda69e8cb9f2277e51a834772142a Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 19 Feb 2026 14:59:30 -0800 Subject: [PATCH 17/20] 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 | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 80ded5dd..81eacc8c 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1058,6 +1058,16 @@ 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 +1104,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 0786015fb5e1c3cbc550206de014c1c400dad074 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 19 Feb 2026 14:59:50 -0800 Subject: [PATCH 18/20] interdiff: Fix crash in ctx_lookahead() on trailing + lines When the last line in the remaining hunk content is a + line, the function ctx_lookahead() skips it without checking for end-of- content. The loop then continues with a NULL next_line pointer, crashing on dereference. Add the same end-of-content check that already exists for non-+ lines. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/interdiff.c b/src/interdiff.c index 81eacc8c..890e48e7 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1498,8 +1498,13 @@ 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 == '+') + if (*line == '+') { + if (!next_line || next_line + 1 == hunk + hlen) { + xctx->num = num; + break; + } continue; + } linelen = next_line + 1 - line; From 7fbd440625fa977ff8c618fcf291fc9e7b4fca5a Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Thu, 19 Feb 2026 15:01:02 -0800 Subject: [PATCH 19/20] 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 post-context at the pre-context count so the fuzz budget is evenly split between prefix and suffix. Assisted-by: Claude Opus 4.6 (1M context) --- src/interdiff.c | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/interdiff.c b/src/interdiff.c index 890e48e7..376fce42 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -1811,17 +1811,33 @@ 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; + + if (suffix > prefix) { + unsigned long trim = suffix - prefix; + + if (trim > nctx[1]) + trim = nctx[1]; + end_line -= trim; + } + } + /* Calculate the old and new line counts */ onum = nnum = xctx_pre.num + /* Extra pre-context */ end_line + 1 - start_line + /* Hunk */ @@ -1871,9 +1887,13 @@ 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. */ + * to be the next split hunk's pre-context. + * Clamp at the fuzz value so the next hunk's + * prefix never causes a negative suffix_fuzz in + * the patch utility. */ start_line_idx -= 1 + nctx[1]; - xctx_pre = (typeof (xctx_pre)){ xctx_post.num }; + xctx_pre = (typeof (xctx_pre)) + { MIN (xctx_post.num, get_fuzz ()) }; if (xctx_pre.num) ctx_lookbehind (lines, start_line_idx, &xctx_pre); From 84a6fe14d874f138eb51a3740e4f01b1b9114092 Mon Sep 17 00:00:00 2001 From: Sultan Alsawaf Date: Fri, 20 Feb 2026 00:01:01 -0800 Subject: [PATCH 20/20] tests: Add fuzzy mode tests for interdiff Add tests fuzzy1 through fuzzy9 exercising the interdiff --fuzzy mode with various patch combinations including relocations, hunk splitting, reject handling, and real-world kernel backport diffs. --- Makefile.am | 11 +- tests/fuzzy1/run-test | 92 +++ tests/fuzzy2/run-test | 42 + tests/fuzzy3/run-test | 56 ++ tests/fuzzy4/run-test | 90 +++ tests/fuzzy5/run-test | 127 +++ tests/fuzzy6/run-test | 56 ++ tests/fuzzy7/run-test | 919 ++++++++++++++++++++++ tests/fuzzy8/run-test | 1736 +++++++++++++++++++++++++++++++++++++++++ tests/fuzzy9/run-test | 294 +++++++ 10 files changed, 3422 insertions(+), 1 deletion(-) create mode 100644 tests/fuzzy1/run-test create mode 100644 tests/fuzzy2/run-test create mode 100644 tests/fuzzy3/run-test create mode 100644 tests/fuzzy4/run-test create mode 100644 tests/fuzzy5/run-test create mode 100644 tests/fuzzy6/run-test create mode 100644 tests/fuzzy7/run-test create mode 100644 tests/fuzzy8/run-test create mode 100644 tests/fuzzy9/run-test diff --git a/Makefile.am b/Makefile.am index 2214bf97..5fc67d37 100644 --- a/Makefile.am +++ b/Makefile.am @@ -432,7 +432,16 @@ 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 # 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 100644 index 00000000..7059869d --- /dev/null +++ b/tests/fuzzy1/run-test @@ -0,0 +1,92 @@ +#!/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/fuzzy2/run-test b/tests/fuzzy2/run-test new file mode 100644 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 100644 index 00000000..b874984e --- /dev/null +++ b/tests/fuzzy3/run-test @@ -0,0 +1,56 @@ +#!/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 100644 index 00000000..aafdfa53 --- /dev/null +++ b/tests/fuzzy4/run-test @@ -0,0 +1,90 @@ +#!/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 100644 index 00000000..09c9bdcc --- /dev/null +++ b/tests/fuzzy5/run-test @@ -0,0 +1,127 @@ +#!/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 +15,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 100644 index 00000000..dfddce57 --- /dev/null +++ b/tests/fuzzy6/run-test @@ -0,0 +1,56 @@ +#!/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 100644 index 00000000..82768ff8 --- /dev/null +++ b/tests/fuzzy7/run-test @@ -0,0 +1,919 @@ +#!/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 100644 index 00000000..8f616c40 --- /dev/null +++ b/tests/fuzzy8/run-test @@ -0,0 +1,1736 @@ +#!/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 100644 index 00000000..9987f0f0 --- /dev/null +++ b/tests/fuzzy9/run-test @@ -0,0 +1,294 @@ +#!/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