55import re
66import sys
77import textwrap
8+ import os
9+ from typing import Optional
810
911def run_git (repo , args ):
1012 """Run a git command in the given repository and return its output as a string."""
@@ -50,14 +52,15 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
5052 """
5153 Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive.
5254 Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length.
55+ Returns a list of tuples: (full_hash, display_string)
5356 """
5457 results = []
5558 # Get all commits with 'Fixes:' in the message
5659 output = run_git (repo , [
5760 'log' , upstream_ref , '--grep' , 'Fixes:' , '-i' , '--format=%H %h %s (%an)%x0a%B%x00'
5861 ]).strip ()
5962 if not output :
60- return ""
63+ return []
6164 # Each commit is separated by a NUL character and a newline
6265 commits = output .split ('\x00 \x0a ' )
6366 # Prepare hash prefixes from 12 down to 6
@@ -78,11 +81,11 @@ def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
7881 for prefix in hash_prefixes :
7982 if m .group (1 ).lower ().startswith (prefix .lower ()):
8083 if not commit_exists_in_branch (repo , pr_branch , full_hash ):
81- results .append (' ' .join (header .split ()[1 :]))
84+ results .append (( full_hash , ' ' .join (header .split ()[1 :]) ))
8285 break
8386 else :
8487 continue
85- return " \n " . join ( results )
88+ return results
8689
8790def commit_exists_in_branch (repo , pr_branch , upstream_hash_ ):
8891 """
@@ -104,17 +107,75 @@ def wrap_paragraph(text, width=80, initial_indent='', subsequent_indent=''):
104107 break_on_hyphens = False )
105108 return wrapper .fill (text )
106109
110+ def extract_cve_from_message (msg ):
111+ """Extract CVE reference from commit message. Returns CVE ID or None.
112+ Only matches 'cve CVE-2025-12345', ignores 'cve-bf' and 'cve-pre' variants."""
113+ match = re .search (r'(?<!\S)cve\s+(CVE-\d{4}-\d+)' , msg , re .IGNORECASE )
114+ if match :
115+ return match .group (1 ).upper ()
116+ return None
117+
118+ def run_cve_search (vulns_repo , kernel_repo , query ) -> tuple [bool , Optional [str ]]:
119+ """
120+ Run the cve_search script from the vulns repo.
121+ Returns (success, output_message).
122+ """
123+ cve_search_path = os .path .join (vulns_repo , 'scripts' , 'cve_search' )
124+ if not os .path .exists (cve_search_path ):
125+ raise RuntimeError (f"cve_search script not found at { cve_search_path } " )
126+
127+ env = os .environ .copy ()
128+ env ['CVEKERNELTREE' ] = kernel_repo
129+
130+ result = subprocess .run ([cve_search_path , query ],
131+ text = True ,
132+ capture_output = True ,
133+ check = False ,
134+ env = env )
135+
136+ # cve_search outputs results to stdout
137+ return result .returncode == 0 , result .stdout .strip ()
138+
107139def main ():
108140 parser = argparse .ArgumentParser (description = "Check upstream references and Fixes: tags in PR branch commits." )
109141 parser .add_argument ("--repo" , help = "Path to the git repo" , required = True )
110142 parser .add_argument ("--pr_branch" , help = "Name of the PR branch" , required = True )
111143 parser .add_argument ("--base_branch" , help = "Name of the base branch" , required = True )
112144 parser .add_argument ("--markdown" , action = 'store_true' , help = "Output in Markdown, suitable for GitHub PR comments" )
113145 parser .add_argument ("--upstream-ref" , default = "origin/kernel-mainline" , help = "Reference to upstream mainline branch (default: origin/kernel-mainline)" )
146+ parser .add_argument ("--check-cves" , action = 'store_true' , help = "Check that CVE references in commit messages match upstream commit hashes" )
147+ parser .add_argument ("--vulns-dir" , default = "../vulns" , help = "Path to the kernel vulnerabilities repo (default: ../vulns)" )
114148 args = parser .parse_args ()
115149
116150 upstream_ref = args .upstream_ref
117151
152+ # Set up vulns repo path if CVE checking is enabled
153+ vulns_repo = None
154+ if args .check_cves :
155+ vulns_repo = args .vulns_dir
156+ vulns_repo_url = "https://git.kernel.org/pub/scm/linux/security/vulns.git"
157+
158+ if os .path .exists (vulns_repo ):
159+ # Repository exists, update it with git pull
160+ try :
161+ run_git (vulns_repo , ['pull' ])
162+ except RuntimeError as e :
163+ print (f"WARNING: Failed to update vulns repo: { e } " )
164+ print ("Continuing with existing repository..." )
165+ else :
166+ # Repository doesn't exist, clone it
167+ try :
168+ result = subprocess .run (['git' , 'clone' , vulns_repo_url , vulns_repo ],
169+ text = True ,
170+ capture_output = True ,
171+ check = False )
172+ if result .returncode != 0 :
173+ print (f"ERROR: Failed to clone vulns repo: { result .stderr } " )
174+ sys .exit (1 )
175+ except Exception as e :
176+ print (f"ERROR: Failed to clone vulns repo: { e } " )
177+ sys .exit (1 )
178+
118179 # Validate that all required refs exist before continuing
119180 missing_refs = []
120181 for refname , refval in [('upstream reference' , upstream_ref ),
@@ -168,8 +229,34 @@ def main():
168229 fixes = find_fixes_in_mainline (args .repo , args .pr_branch , upstream_ref , uhash )
169230 if fixes :
170231 any_findings = True
232+
233+ # Check CVEs for bugfix commits if enabled
234+ fix_cves = {}
235+ if args .check_cves :
236+ for fix_hash , fix_display in fixes :
237+ try :
238+ success , cve_output = run_cve_search (vulns_repo , args .repo , fix_hash )
239+ if success :
240+ # Parse the CVE from the result
241+ match = re .search (r'(CVE-\d{4}-\d+)\s+is assigned to git id' , cve_output )
242+ if match :
243+ bugfix_cve = match .group (1 )
244+ fix_cves [fix_hash ] = bugfix_cve
245+ except (RuntimeError , subprocess .SubprocessError ) as e :
246+ # Log a warning instead of silently ignoring errors when checking bugfix CVEs
247+ print (f"Warning: Failed to check CVE for bugfix commit { fix_hash } : { e } " , file = sys .stderr )
248+
249+ # Build the fixes display text with CVE info
250+ fixes_lines = []
251+ for fix_hash , display_str in fixes :
252+ if fix_hash in fix_cves :
253+ fixes_lines .append (f"{ display_str } ({ fix_cves [fix_hash ]} )" )
254+ else :
255+ fixes_lines .append (display_str )
256+ fixes_text = "\n " .join (fixes_lines )
257+
171258 if args .markdown :
172- fixes_block = " " + fixes .replace ("\n " , "\n " )
259+ fixes_block = " " + fixes_text .replace ("\n " , "\n " )
173260 out_lines .append (
174261 f"- ⚠️ PR commit `{ pr_commit_desc } ` references upstream commit \n "
175262 f" `{ short_uhash } ` which has been referenced by a `Fixes:` tag in the upstream \n "
@@ -185,10 +272,97 @@ def main():
185272 subsequent_indent = ' ' * len (prefix )) # spaces for '[FIXES] '
186273 )
187274 out_lines .append ("" ) # blank line after 'Fixes tags:'
188- for line in fixes .splitlines ():
275+ for line in fixes_text .splitlines ():
189276 out_lines .append (' ' + line )
190277 out_lines .append ("" ) # blank line
191278
279+ # Check CVE if enabled
280+ if args .check_cves :
281+ cve_id = extract_cve_from_message (msg )
282+
283+ # Check if the upstream commit has a CVE associated with it
284+ try :
285+ success , cve_output = run_cve_search (vulns_repo , args .repo , uhash )
286+ if success :
287+ # Parse the output to get the CVE from the result
288+ # Expected format: "CVE-2024-35962 is assigned to git id 65acf6e0501ac8880a4f73980d01b5d27648b956"
289+ match = re .search (r'(CVE-\d{4}-\d+)\s+is assigned to git id' , cve_output )
290+ if match :
291+ found_cve = match .group (1 )
292+
293+ if cve_id :
294+ # PR commit has a CVE reference - check if it matches
295+ if found_cve != cve_id :
296+ any_findings = True
297+ if args .markdown :
298+ out_lines .append (
299+ f"- ❌ PR commit `{ pr_commit_desc } ` references `{ cve_id } ` but \n "
300+ f" upstream commit `{ short_uhash } ` is associated with `{ found_cve } `\n "
301+ )
302+ else :
303+ prefix = "[CVE-MISMATCH] "
304+ header = (f"{ prefix } PR commit { pr_commit_desc } references { cve_id } but "
305+ f"upstream commit { short_uhash } is associated with { found_cve } " )
306+ out_lines .append (
307+ wrap_paragraph (header , width = 80 , initial_indent = '' ,
308+ subsequent_indent = ' ' * len (prefix ))
309+ )
310+ out_lines .append ("" ) # blank line
311+ else :
312+ # PR commit doesn't reference a CVE, but upstream has one
313+ any_findings = True
314+ if args .markdown :
315+ out_lines .append (
316+ f"- ⚠️ PR commit `{ pr_commit_desc } ` does not reference a CVE but \n "
317+ f" upstream commit `{ short_uhash } ` is associated with `{ found_cve } `\n "
318+ )
319+ else :
320+ prefix = "[CVE-MISSING] "
321+ header = (f"{ prefix } PR commit { pr_commit_desc } does not reference a CVE but "
322+ f"upstream commit { short_uhash } is associated with { found_cve } " )
323+ out_lines .append (
324+ wrap_paragraph (header , width = 80 , initial_indent = '' ,
325+ subsequent_indent = ' ' * len (prefix ))
326+ )
327+ out_lines .append ("" ) # blank line
328+ else :
329+ # The upstream commit has no CVE assigned
330+ if cve_id :
331+ # PR commit claims a CVE but upstream has none
332+ any_findings = True
333+ if args .markdown :
334+ out_lines .append (
335+ f"- ❌ PR commit `{ pr_commit_desc } ` references `{ cve_id } ` but \n "
336+ f" upstream commit `{ short_uhash } ` has no CVE assigned\n "
337+ )
338+ else :
339+ prefix = "[CVE-NOTFOUND] "
340+ header = (f"{ prefix } PR commit { pr_commit_desc } references { cve_id } but "
341+ f"upstream commit { short_uhash } has no CVE assigned" )
342+ out_lines .append (
343+ wrap_paragraph (header , width = 80 , initial_indent = '' ,
344+ subsequent_indent = ' ' * len (prefix ))
345+ )
346+ out_lines .append ("" ) # blank line
347+ except (subprocess .SubprocessError , OSError ) as e :
348+ # Error running cve_search
349+ if cve_id :
350+ any_findings = True
351+ if args .markdown :
352+ out_lines .append (
353+ f"- ⚠️ PR commit `{ pr_commit_desc } ` references `{ cve_id } ` but \n "
354+ f" failed to verify: { e } \n "
355+ )
356+ else :
357+ prefix = "[CVE-ERROR] "
358+ header = (f"{ prefix } PR commit { pr_commit_desc } references { cve_id } but "
359+ f"failed to verify: { e } " )
360+ out_lines .append (
361+ wrap_paragraph (header , width = 80 , initial_indent = '' ,
362+ subsequent_indent = ' ' * len (prefix ))
363+ )
364+ out_lines .append ("" ) # blank line
365+
192366 if any_findings :
193367 if args .markdown :
194368 print ("## :mag: Upstream Linux Kernel Commit Check\n " )
0 commit comments