[PATCH] checkpatch: Validate committer sign-off
From: Bjorn Andersson <hidden>
Date: 2026-07-24 18:56:09
Also in:
linux-doc, lkml, workflows
Subsystem:
checkpatch, checkpatch documentation, documentation, documentation process, the rest · Maintainers:
Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn, Jonathan Corbet, Linus Torvalds
checkpatch validates the Signed-off-by trailers against the patch's author, but misses the opportunity to also validate the committer when run with --git. Maintainers therefor need third-party scripts for the final Signed-off-by check. git format-patch provides the author and patch content, but not the committer. Extend the existing git log extraction to record the committer identity. Extract the author sign-off comparison logic into a shared helper, and use it to apply the same heuristics to the final Signed-off-by trailer and committer. Emit COMMITTER_SIGN_OFF_MISMATCH when the final trailer does not match the committer. Assisted-by: OpenCode:GPT-5.5 Signed-off-by: Bjorn Andersson <redacted> --- Documentation/dev-tools/checkpatch.rst | 5 ++ scripts/checkpatch.pl | 112 ++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 35 deletions(-)
diff --git a/Documentation/dev-tools/checkpatch.rst b/Documentation/dev-tools/checkpatch.rst
index 6139a08c34cd..7726c3863bcc 100644
--- a/Documentation/dev-tools/checkpatch.rst
+++ b/Documentation/dev-tools/checkpatch.rst@@ -562,6 +562,11 @@ Commit message - The email subaddresses do not match. - The email comments do not match. + **COMMITTER_SIGN_OFF_MISMATCH** + When checking a Git commit, the final Signed-off-by: line does not match + the commit's committer. The person committing a patch should add their + own Signed-off-by: line last. + **MISSING_SIGN_OFF** The patch is missing a Signed-off-by line. A signed-off-by line should be added according to Developer's certificate of
diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl
index 7a846a3ea127..0e1bff241597 100755
--- a/scripts/checkpatch.pl
+++ b/scripts/checkpatch.pl@@ -37,6 +37,7 @@ my $showfile = 0; my $file = 0; my $git = 0; my %git_commits = (); +my %git_committers = (); my $check = 0; my $check_orig = 0; my $summary = 1;
@@ -1317,14 +1318,16 @@ if ($git) { } else { $git_range = "-1 $commit_expr"; } - my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%H %s' $git_range`; + my $lines = `${git_command} log --no-color --no-merges --pretty=format:'%cn <%ce> %H %s' $git_range`; foreach my $line (split(/\n/, $lines)) { - $line =~ /^([0-9a-fA-F]{40,40}) (.*)$/; - next if (!defined($1) || !defined($2)); - my $sha1 = $1; - my $subject = $2; + $line =~ /^(.* <[^>]+>) ([0-9a-fA-F]{40,40}) (.*)$/; + next if (!defined($1) || !defined($2) || !defined($3)); + my $committer = $1; + my $sha1 = $2; + my $subject = $3; unshift(@commits, $sha1); $git_commits{$sha1} = $subject; + $git_committers{$sha1} = $committer; } } die "$P: no git commits after extraction!\n" if (@commits == 0);
@@ -1530,6 +1533,43 @@ sub same_email_addresses { $comment1 eq $comment2; } +sub signoff_match_status { + my ($signoff, $identity) = @_; + my $authorsignoff = 0; + + if (same_email_addresses($signoff, $identity)) { + $authorsignoff = 1; + } else { + my ($signoff_name, $signoff_comment, $signoff_address, $comment1) = parse_email($signoff); + my ($identity_name, $identity_comment, $identity_address, $comment2) = parse_email($identity); + + if (lc $signoff_address eq lc $identity_address && + $signoff_name eq $identity_name) { + $authorsignoff = 2; + } elsif (lc $signoff_address eq lc $identity_address) { + $authorsignoff = 3; + } elsif ($signoff_name eq $identity_name) { + $authorsignoff = 4; + + my $address1 = $signoff_address; + my $address2 = $identity_address; + + if ($address1 =~ /(\S+)\+\S+(\@.*)/) { + $address1 = "$1$2"; + } + if ($address2 =~ /(\S+)\+\S+(\@.*)/) { + $address2 = "$1$2"; + } + + if ($address1 eq $address2) { + $authorsignoff = 5; + } + } + } + + return $authorsignoff; +} + sub which { my ($bin) = @_;
@@ -2690,6 +2730,9 @@ sub process { my $author = ''; my $authorsignoff = 0; my $author_sob = ''; + my $last_signoff = ''; + my $committer = $git_committers{$filename} // ''; + my $committersignoff = 0; my $is_patch = 0; my $is_binding_patch = -1; my $in_header_lines = $file ? 0 : 1;
@@ -3018,40 +3061,16 @@ sub process { # Check the patch for a signoff: if ($line =~ /^\s*signed-off-by:\s*(.*)/i) { $signoff++; + $last_signoff = $1; $in_commit_log = 0; if ($author ne '' && $authorsignoff != 1) { - if (same_email_addresses($1, $author)) { - $authorsignoff = 1; - } else { - my $ctx = $1; - my ($email_name, $email_comment, $email_address, $comment1) = parse_email($ctx); - my ($author_name, $author_comment, $author_address, $comment2) = parse_email($author); - - if (lc $email_address eq lc $author_address && $email_name eq $author_name) { - $author_sob = $ctx; - $authorsignoff = 2; - } elsif (lc $email_address eq lc $author_address) { - $author_sob = $ctx; - $authorsignoff = 3; - } elsif ($email_name eq $author_name) { - $author_sob = $ctx; - $authorsignoff = 4; - - my $address1 = $email_address; - my $address2 = $author_address; - - if ($address1 =~ /(\S+)\+\S+(\@.*)/) { - $address1 = "$1$2"; - } - if ($address2 =~ /(\S+)\+\S+(\@.*)/) { - $address2 = "$1$2"; - } - if ($address1 eq $address2) { - $authorsignoff = 5; - } - } + my $signoff_status = signoff_match_status($1, $author); + if ($signoff_status != 0) { + $authorsignoff = $signoff_status; + $author_sob = $1 if ($signoff_status != 1); } } + $committersignoff = signoff_match_status($1, $committer) if ($committer ne ''); } # Check for invalid patch separator
@@ -7901,6 +7920,29 @@ sub process { } } } + if ($is_patch && $has_commit_log && $chk_signoff && + $committer ne '' && + $last_signoff ne '' && + $committersignoff != 1) { + my $sob_msg = "'Committer: $committer' != 'Signed-off-by: $last_signoff'"; + + if ($committersignoff == 2) { + CHK("COMMITTER_SIGN_OFF_MISMATCH", + "Committer:/Signed-off-by: email comments mismatch: $sob_msg\n"); + } elsif ($committersignoff == 3) { + WARN("COMMITTER_SIGN_OFF_MISMATCH", + "Committer:/Signed-off-by: email name mismatch: $sob_msg\n"); + } elsif ($committersignoff == 4) { + WARN("COMMITTER_SIGN_OFF_MISMATCH", + "Committer:/Signed-off-by: email address mismatch: $sob_msg\n"); + } elsif ($committersignoff == 5) { + WARN("COMMITTER_SIGN_OFF_MISMATCH", + "Committer:/Signed-off-by: email subaddress mismatch: $sob_msg\n"); + } else { + WARN("COMMITTER_SIGN_OFF_MISMATCH", + "Last Signed-off-by: '$last_signoff' != commit committer '$committer'\n"); + } + } print report_dump(); if ($summary && !($clean == 1 && $quiet == 1)) {
--- base-commit: 290aaf24a551d5a0dce037e3fab30820f9113a10 change-id: 20260723-checkpatch-committer-sob-10fcbd636d43 Best regards, -- Bjorn Andersson [off-list ref]